Baseline Lint FlaggedApi violations am: 8fc03b46b1

Original change: https://android-review.googlesource.com/c/platform/packages/modules/AppSearch/+/3140988

Change-Id: I0bad49e6731e0662087008bd3bdd52aff6da57ef
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/Android.bp b/Android.bp
index c53972f..290f036 100644
--- a/Android.bp
+++ b/Android.bp
@@ -48,6 +48,7 @@
     jni_libs: ["libicing"],
     prebuilts: ["current_sdkinfo"],
     min_sdk_version: "33",
+    apps: ["com.android.appsearch.apk"],
 }
 
 apex_key {
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..71e8dd8
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,10 @@
+[Builtin Hooks]
+google_java_format = true
+bpfmt = true
+
+[Builtin Hooks Options]
+bpfmt = -s
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 9da9441..fa0bcbb 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -17,6 +17,9 @@
     },
     {
       "name": "AppSearchMockingServicesTests"
+    },
+    {
+      "name": "AppsIndexerTests"
     }
   ]
 }
diff --git a/apk/Android.bp b/apk/Android.bp
new file mode 100644
index 0000000..e9b6a51
--- /dev/null
+++ b/apk/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app_certificate {
+    name: "com.android.appsearch.apk.certificate",
+    certificate: "com.android.appsearch.apk",
+}
+
+android_app {
+    name: "com.android.appsearch.apk",
+    sdk_version: "module_current",
+    min_sdk_version: "33",
+    privileged: true,
+    updatable: true,
+    certificate: ":com.android.appsearch.apk.certificate",
+    apex_available: ["com.android.appsearch"],
+}
diff --git a/apk/AndroidManifest.xml b/apk/AndroidManifest.xml
new file mode 100644
index 0000000..b649a52
--- /dev/null
+++ b/apk/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.appsearch.apk">
+
+    <!-- Must be required by a {@link android.app.appsearch.functions.AppFunctionService},
+         to ensure that only the system can bind to it.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.BIND_APP_FUNCTION_SERVICE"
+                android:protectionLevel="signature"/>
+    <!-- Allows system applications to execute app functions provided by apps through AppSearch. -->
+    <permission android:name="android.permission.EXECUTE_APP_FUNCTION"
+                android:protectionLevel="internal|role" />
+
+</manifest>
\ No newline at end of file
diff --git a/apk/com.android.appsearch.apk.pk8 b/apk/com.android.appsearch.apk.pk8
new file mode 100644
index 0000000..f86a478
--- /dev/null
+++ b/apk/com.android.appsearch.apk.pk8
Binary files differ
diff --git a/apk/com.android.appsearch.apk.x509.pem b/apk/com.android.appsearch.apk.x509.pem
new file mode 100644
index 0000000..409cf10
--- /dev/null
+++ b/apk/com.android.appsearch.apk.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID8TCCAtmgAwIBAgIUBklv1rrnZwmuia/G2NLML0fB61YwDQYJKoZIhvcNAQEL
+BQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwH
+QW5kcm9pZDEiMCAGA1UEAwwZY29tLmFuZHJvaWQuYXBwc2VhcmNoLmFwazAgFw0y
+NDAzMDgxNTMwMTZaGA8yMDUxMDcyNTE1MzAxNlowgYYxCzAJBgNVBAYTAlVTMRMw
+EQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRQwEgYD
+VQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEiMCAGA1UEAwwZY29t
+LmFuZHJvaWQuYXBwc2VhcmNoLmFwazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBALVRwc5fjILugcJofjmnwvZ8/pCjy6Zzk5MDzpgwXjwsMn1jfU8XRGpQ
+NaNLN24Sl54cph378ex0ClET8mHYVkoXlFZymHFAX43Z2/pE7uWkANEzGZPToobS
+JxOPUJH+vmh5YVrHH3anAuO4+CUOClYLaGhxc7F+gV9QM5vw+bv+XUd0M3tZLPYp
+JdZLKSkfMseKmb5K0Yt6Sm3IzCBUBRuCV1jbSZjUpHzsmJpU+O8PTJzlyQDPpppS
+tCswPs6xxAfxZ6qYdiVFT/BmWyUuwniiw5J4xrOo6vlikSRJ+GoihV4cFs/RfQf9
+NT0UtBapUKK+j3KIXmA9D/YqJludhbkCAwEAAaNTMFEwHQYDVR0OBBYEFE0KNxLe
+7476HprBqKe7G5VdHBmKMB8GA1UdIwQYMBaAFE0KNxLe7476HprBqKe7G5VdHBmK
+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAE8lTUk+ThLxhsDp
+IZKJBmWrekN669bL7XLoty4lp/nAbSwWixucPL1FnzaL0FMWvqr4OvKVzwLdHAig
+y4D7h5xULpLmfljWt01hrtKnnwnONyf7e0YTZkNnKav31+5auQRBbsL/cgZaGjhS
+E6tpv7ERmHbH666suBP7V8R4qWnSyT1HZAJhJoQU1tgtEOe4mxrmwoNUalpTEPuh
++kJP7LkR6nXhRsXdiYhhuC9qHRnqo4+zSVXpE2Xlkmstc0GhCoZSVGPA0+0gKDqs
+RXqYTa2f/cAbcvgVj+RZyofp5UVOWA+BIuUOlvvrfMTXqbjN96RL5yy4foMfPIQ0
+l/VzO3E=
+-----END CERTIFICATE-----
diff --git a/flags/Android.bp b/flags/Android.bp
index 7582f5b..4655384 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -9,6 +9,7 @@
     srcs: [
         "appsearch.aconfig",
     ],
+    exportable: true,
 }
 
 java_aconfig_library {
@@ -19,4 +20,7 @@
         "//packages/modules/AppSearch:__subpackages__",
     ],
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    mode: "exported",
+    min_sdk_version: "Tiramisu",
+    apex_available: ["com.android.appsearch"],
 }
diff --git a/flags/appsearch.aconfig b/flags/appsearch.aconfig
index 36b5259..206b2b8 100644
--- a/flags/appsearch.aconfig
+++ b/flags/appsearch.aconfig
@@ -7,6 +7,7 @@
 
 flag {
     name: "enable_safe_parcelable_2"
+    is_exported: true
     namespace: "appsearch"
     description: "Use SafeParcelable to serialize data through binder"
     bug: "275629842"
@@ -15,6 +16,7 @@
 
 flag {
     name: "enable_grouping_type_per_schema"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable result grouping by schema type"
     bug: "258715421"
@@ -23,6 +25,7 @@
 
 flag {
     name: "enable_generic_document_copy_constructor"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable GenericDocument to take another GenericDocument to copy construct."
     bug: "171882200"
@@ -31,6 +34,7 @@
 
 flag {
     name: "enable_list_filter_has_property_function"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable the hasProperty function in list filter query expressions."
     bug: "309826655"
@@ -39,6 +43,7 @@
 
 flag {
     name: "enable_put_documents_request_add_taken_actions"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable addTakenActions API in PutDocumentsRequest."
     bug: "314026345"
@@ -47,6 +52,7 @@
 
 flag {
     name: "enable_set_publicly_visible_schema"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable setPubliclyVisibleSchema API in SetSchemaRequest."
     bug: "325262525"
@@ -55,6 +61,7 @@
 
 flag {
     name: "enable_generic_document_builder_hidden_methods"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable GenericDocument.Builder to use previously hidden methods."
     bug: "318408639"
@@ -63,6 +70,7 @@
 
 flag {
     name: "enable_search_spec_filter_properties"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable addFilterProperties API in SearchSpec and SearchSuggestionSpec."
     bug: "296088047"
@@ -71,6 +79,7 @@
 
 flag {
     name: "enable_search_spec_set_search_source_log_tag"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable the setSearchSourceLogTag API in SearchSpec."
     bug: "315370764"
@@ -79,6 +88,7 @@
 
 flag {
     name: "enable_enterprise_global_search_session"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable personal profile to search over allowed enterprise profile data in AppSearch through enterprise global search session."
     bug: "237388235"
@@ -87,8 +97,64 @@
 
 flag {
     name: "enable_set_schema_visible_to_configs"
+    is_exported: true
     namespace: "appsearch"
     description: "Enable the addSchemaTypeVisibleToConfig API in the setSchemaRequest."
     bug: "300162279"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "enable_app_functions"
+    namespace: "appsearch"
+    description: "Guards everything related to AppFunction."
+    bug: "327134039"
+    is_fixed_read_only: true
+    is_exported: true
+}
+
+flag {
+    name: "enable_result_denied_and_result_rate_limited"
+    namespace: "appsearch"
+    description: "Enable previously hidden constants in AppSearchResult."
+    bug: "279047435"
+    is_fixed_read_only: true
+    is_exported: true
+}
+
+flag {
+    name: "enable_get_parent_types_and_indexable_nested_properties"
+    namespace: "appsearch"
+    description: "Enable previously hidden APIs in AppSearchSchema."
+    bug: "291122592"
+    is_fixed_read_only: true
+    is_exported: true
+}
+
+flag {
+    name: "enable_schema_embedding_property_config"
+    namespace: "appsearch"
+    description: "Enables embedding vectors to be used for searches in AppSearch"
+    bug: "326656531"
+    is_fixed_read_only: true
+    is_exported: true
+}
+
+flag {
+    name: "enable_informational_ranking_expressions"
+    namespace: "appsearch"
+    description: "Enables the additional informational ranking expression API"
+    bug: "332642571"
+    is_fixed_read_only: true
+    is_exported: true
+}
+
+
+flag {
+    name: "enable_list_filter_tokenize_function"
+    namespace: "appsearch"
+    description: "Enables the tokenize function in the list filter language"
+    bug: "332620561"
+    is_fixed_read_only: true
+    is_exported: true
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index c8a16c1..89e938c 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -25,8 +25,8 @@
     name: "framework-appsearch-sources",
     defaults: ["framework-sources-module-defaults"],
     srcs: [
-        ":framework-appsearch-internal-sources",
         ":framework-appsearch-external-sources",
+        ":framework-appsearch-internal-sources",
     ],
     visibility: ["//packages/modules/AppSearch:__subpackages__"],
 }
@@ -34,8 +34,8 @@
 filegroup {
     name: "framework-appsearch-internal-sources",
     srcs: [
-        "java/**/*.java",
         "java/**/*.aidl",
+        "java/**/*.java",
     ],
     exclude_srcs: [
         ":framework-appsearch-external-sources",
@@ -46,8 +46,8 @@
 filegroup {
     name: "framework-appsearch-external-sources",
     srcs: [
-        "java/external/**/*.java",
         "java/external/**/*.aidl",
+        "java/external/**/*.java",
     ],
     path: "java/external",
 }
@@ -62,19 +62,23 @@
     ],
     static_libs: [
         // This list must be kept in sync with jarjar.txt
+        "appsearch_flags_java_lib",
         "modules-utils-preconditions",
     ],
     optimize: {
-      enabled: true,
-      optimize: true,
-      shrink: true,
-      proguard_flags_files: ["proguard.flags"],
+        enabled: true,
+        optimize: true,
+        shrink: true,
+        proguard_flags_files: ["proguard.flags"],
     },
     plugins: [
         "safeparcel-annotation-processor",
     ],
     defaults: ["framework-module-defaults"],
-    permitted_packages: ["android.app.appsearch"],
+    permitted_packages: [
+        "android.app.appsearch",
+        "com.android.appsearch.flags",
+    ],
     jarjar_rules: "jarjar-rules.txt",
     apex_available: ["com.android.appsearch"],
     impl_library_visibility: [
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 7d69ef3..9b76934 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -20,6 +20,7 @@
     method @FlaggedApi("com.android.appsearch.flags.enable_enterprise_global_search_session") public void createEnterpriseGlobalSearchSession(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appsearch.AppSearchResult<android.app.appsearch.EnterpriseGlobalSearchSession>>);
     method public void createGlobalSearchSession(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appsearch.AppSearchResult<android.app.appsearch.GlobalSearchSession>>);
     method public void createSearchSession(@NonNull android.app.appsearch.AppSearchManager.SearchContext, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appsearch.AppSearchResult<android.app.appsearch.AppSearchSession>>);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.functions.AppFunctionManager getAppFunctionManager();
   }
 
   public static final class AppSearchManager.SearchContext {
@@ -38,6 +39,7 @@
     method public boolean isSuccess();
     method @NonNull public static <ValueType> android.app.appsearch.AppSearchResult<ValueType> newFailedResult(int, @Nullable String);
     method @NonNull public static <ValueType> android.app.appsearch.AppSearchResult<ValueType> newSuccessfulResult(@Nullable ValueType);
+    field @FlaggedApi("com.android.appsearch.flags.enable_result_denied_and_result_rate_limited") public static final int RESULT_DENIED = 9; // 0x9
     field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
     field public static final int RESULT_INVALID_ARGUMENT = 3; // 0x3
     field public static final int RESULT_INVALID_SCHEMA = 7; // 0x7
@@ -45,12 +47,16 @@
     field public static final int RESULT_NOT_FOUND = 6; // 0x6
     field public static final int RESULT_OK = 0; // 0x0
     field public static final int RESULT_OUT_OF_SPACE = 5; // 0x5
+    field @FlaggedApi("com.android.appsearch.flags.enable_result_denied_and_result_rate_limited") public static final int RESULT_RATE_LIMITED = 10; // 0xa
     field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+    field @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public static final int RESULT_TIMED_OUT = 11; // 0xb
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
   public final class AppSearchSchema implements android.os.Parcelable {
     method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public String getDescription();
+    method @FlaggedApi("com.android.appsearch.flags.enable_get_parent_types_and_indexable_nested_properties") @NonNull public java.util.List<java.lang.String> getParentTypes();
     method @NonNull public java.util.List<android.app.appsearch.AppSearchSchema.PropertyConfig> getProperties();
     method @NonNull public String getSchemaType();
     method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -64,6 +70,7 @@
     ctor public AppSearchSchema.BooleanPropertyConfig.Builder(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.BooleanPropertyConfig build();
     method @NonNull public android.app.appsearch.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(@NonNull String);
   }
 
   public static final class AppSearchSchema.Builder {
@@ -71,6 +78,7 @@
     method @NonNull public android.app.appsearch.AppSearchSchema.Builder addParentType(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.Builder addProperty(@NonNull android.app.appsearch.AppSearchSchema.PropertyConfig);
     method @NonNull public android.app.appsearch.AppSearchSchema build();
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.Builder setDescription(@NonNull String);
   }
 
   public static final class AppSearchSchema.BytesPropertyConfig extends android.app.appsearch.AppSearchSchema.PropertyConfig {
@@ -80,18 +88,24 @@
     ctor public AppSearchSchema.BytesPropertyConfig.Builder(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.BytesPropertyConfig build();
     method @NonNull public android.app.appsearch.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.BytesPropertyConfig.Builder setDescription(@NonNull String);
   }
 
   public static final class AppSearchSchema.DocumentPropertyConfig extends android.app.appsearch.AppSearchSchema.PropertyConfig {
+    method @FlaggedApi("com.android.appsearch.flags.enable_get_parent_types_and_indexable_nested_properties") @NonNull public java.util.List<java.lang.String> getIndexableNestedProperties();
     method @NonNull public String getSchemaType();
     method public boolean shouldIndexNestedProperties();
   }
 
   public static final class AppSearchSchema.DocumentPropertyConfig.Builder {
     ctor public AppSearchSchema.DocumentPropertyConfig.Builder(@NonNull String, @NonNull String);
+    method @FlaggedApi("com.android.appsearch.flags.enable_get_parent_types_and_indexable_nested_properties") @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedProperties(@NonNull java.lang.String...);
     method @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedProperties(@NonNull java.util.Collection<java.lang.String>);
+    method @FlaggedApi("com.android.appsearch.flags.enable_get_parent_types_and_indexable_nested_properties") @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(@NonNull android.app.appsearch.PropertyPath...);
+    method @FlaggedApi("com.android.appsearch.flags.enable_get_parent_types_and_indexable_nested_properties") @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(@NonNull java.util.Collection<android.app.appsearch.PropertyPath>);
     method @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig build();
     method @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
   }
 
@@ -102,6 +116,21 @@
     ctor public AppSearchSchema.DoublePropertyConfig.Builder(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.DoublePropertyConfig build();
     method @NonNull public android.app.appsearch.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.DoublePropertyConfig.Builder setDescription(@NonNull String);
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public static final class AppSearchSchema.EmbeddingPropertyConfig extends android.app.appsearch.AppSearchSchema.PropertyConfig {
+    method public int getIndexingType();
+    field public static final int INDEXING_TYPE_NONE = 0; // 0x0
+    field public static final int INDEXING_TYPE_SIMILARITY = 1; // 0x1
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+    ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(@NonNull String);
+    method @NonNull public android.app.appsearch.AppSearchSchema.EmbeddingPropertyConfig build();
+    method @NonNull public android.app.appsearch.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(@NonNull String);
+    method @NonNull public android.app.appsearch.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
   }
 
   public static final class AppSearchSchema.LongPropertyConfig extends android.app.appsearch.AppSearchSchema.PropertyConfig {
@@ -114,11 +143,13 @@
     ctor public AppSearchSchema.LongPropertyConfig.Builder(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.LongPropertyConfig build();
     method @NonNull public android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder setDescription(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
   }
 
   public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public String getDescription();
     method @NonNull public String getName();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -144,6 +175,7 @@
     ctor public AppSearchSchema.StringPropertyConfig.Builder(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.StringPropertyConfig build();
     method @NonNull public android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_app_functions") @NonNull public android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder setDescription(@NonNull String);
     method @NonNull public android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
     method @NonNull public android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
     method @NonNull public android.app.appsearch.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -169,6 +201,15 @@
     method public default void onSystemError(@Nullable Throwable);
   }
 
+  @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public final class EmbeddingVector implements android.os.Parcelable {
+    ctor public EmbeddingVector(@NonNull float[], @NonNull String);
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
+    method @NonNull public String getModelSignature();
+    method @NonNull public float[] getValues();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.EmbeddingVector> CREATOR;
+  }
+
   @FlaggedApi("com.android.appsearch.flags.enable_enterprise_global_search_session") public class EnterpriseGlobalSearchSession {
     method public void getByDocumentId(@NonNull String, @NonNull String, @NonNull android.app.appsearch.GetByDocumentIdRequest, @NonNull java.util.concurrent.Executor, @NonNull android.app.appsearch.BatchResultCallback<java.lang.String,android.app.appsearch.GenericDocument>);
     method public void getSchema(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appsearch.AppSearchResult<android.app.appsearch.GetSchemaResponse>>);
@@ -190,6 +231,8 @@
     method @Nullable public android.app.appsearch.GenericDocument[] getPropertyDocumentArray(@NonNull String);
     method public double getPropertyDouble(@NonNull String);
     method @Nullable public double[] getPropertyDoubleArray(@NonNull String);
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @Nullable public android.app.appsearch.EmbeddingVector getPropertyEmbedding(@NonNull String);
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @Nullable public android.app.appsearch.EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String);
     method public long getPropertyLong(@NonNull String);
     method @Nullable public long[] getPropertyLongArray(@NonNull String);
     method @NonNull public java.util.Set<java.lang.String> getPropertyNames();
@@ -212,6 +255,7 @@
     method @NonNull public BuilderType setPropertyBytes(@NonNull String, @NonNull byte[]...);
     method @NonNull public BuilderType setPropertyDocument(@NonNull String, @NonNull android.app.appsearch.GenericDocument...);
     method @NonNull public BuilderType setPropertyDouble(@NonNull String, @NonNull double...);
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @NonNull public BuilderType setPropertyEmbedding(@NonNull String, @NonNull android.app.appsearch.EmbeddingVector...);
     method @NonNull public BuilderType setPropertyLong(@NonNull String, @NonNull long...);
     method @NonNull public BuilderType setPropertyString(@NonNull String, @NonNull java.lang.String...);
     method @FlaggedApi("com.android.appsearch.flags.enable_generic_document_builder_hidden_methods") @NonNull public BuilderType setSchemaType(@NonNull String);
@@ -219,11 +263,14 @@
     method @NonNull public BuilderType setTtlMillis(long);
   }
 
-  public final class GetByDocumentIdRequest {
+  public final class GetByDocumentIdRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
     method @NonNull public java.util.Set<java.lang.String> getIds();
     method @NonNull public String getNamespace();
     method @NonNull public java.util.Map<java.lang.String,java.util.List<android.app.appsearch.PropertyPath>> getProjectionPaths();
     method @NonNull public java.util.Map<java.lang.String,java.util.List<java.lang.String>> getProjections();
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.GetByDocumentIdRequest> CREATOR;
     field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
   }
 
@@ -339,9 +386,12 @@
     method @NonNull public android.app.appsearch.PutDocumentsRequest build();
   }
 
-  public final class RemoveByDocumentIdRequest {
+  public final class RemoveByDocumentIdRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
     method @NonNull public java.util.Set<java.lang.String> getIds();
     method @NonNull public String getNamespace();
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.RemoveByDocumentIdRequest> CREATOR;
   }
 
   public static final class RemoveByDocumentIdRequest.Builder {
@@ -365,10 +415,13 @@
     method @NonNull public android.app.appsearch.ReportSystemUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
-  public final class ReportUsageRequest {
+  public final class ReportUsageRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
     method @NonNull public String getDocumentId();
     method @NonNull public String getNamespace();
     method public long getUsageTimestampMillis();
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.ReportUsageRequest> CREATOR;
   }
 
   public static final class ReportUsageRequest.Builder {
@@ -400,6 +453,7 @@
     method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
     method @NonNull public String getDatabaseName();
     method @NonNull public android.app.appsearch.GenericDocument getGenericDocument();
+    method @FlaggedApi("com.android.appsearch.flags.enable_informational_ranking_expressions") @NonNull public java.util.List<java.lang.Double> getInformationalRankingSignals();
     method @NonNull public java.util.List<android.app.appsearch.SearchResult> getJoinedResults();
     method @NonNull public java.util.List<android.app.appsearch.SearchResult.MatchInfo> getMatchInfos();
     method @NonNull public String getPackageName();
@@ -410,6 +464,7 @@
 
   public static final class SearchResult.Builder {
     ctor public SearchResult.Builder(@NonNull String, @NonNull String);
+    method @FlaggedApi("com.android.appsearch.flags.enable_informational_ranking_expressions") @NonNull public android.app.appsearch.SearchResult.Builder addInformationalRankingSignal(double);
     method @NonNull public android.app.appsearch.SearchResult.Builder addJoinedResult(@NonNull android.app.appsearch.SearchResult);
     method @NonNull public android.app.appsearch.SearchResult.Builder addMatchInfo(@NonNull android.app.appsearch.SearchResult.MatchInfo);
     method @NonNull public android.app.appsearch.SearchResult build();
@@ -454,10 +509,12 @@
   public final class SearchSpec implements android.os.Parcelable {
     method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
     method @NonNull public String getAdvancedRankingExpression();
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public int getDefaultEmbeddingSearchMetricType();
     method @NonNull public java.util.List<java.lang.String> getFilterNamespaces();
     method @NonNull public java.util.List<java.lang.String> getFilterPackageNames();
     method @FlaggedApi("com.android.appsearch.flags.enable_search_spec_filter_properties") @NonNull public java.util.Map<java.lang.String,java.util.List<java.lang.String>> getFilterProperties();
     method @NonNull public java.util.List<java.lang.String> getFilterSchemas();
+    method @FlaggedApi("com.android.appsearch.flags.enable_informational_ranking_expressions") @NonNull public java.util.List<java.lang.String> getInformationalRankingExpressions();
     method @Nullable public android.app.appsearch.JoinSpec getJoinSpec();
     method public int getMaxSnippetSize();
     method public int getOrder();
@@ -469,16 +526,22 @@
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
     method public int getResultGroupingTypeFlags();
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @NonNull public java.util.List<android.app.appsearch.EmbeddingVector> getSearchEmbeddings();
     method @FlaggedApi("com.android.appsearch.flags.enable_search_spec_set_search_source_log_tag") @Nullable public String getSearchSourceLogTag();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public boolean isEmbeddingSearchEnabled();
     method @FlaggedApi("com.android.appsearch.flags.enable_list_filter_has_property_function") public boolean isListFilterHasPropertyFunctionEnabled();
     method public boolean isListFilterQueryLanguageEnabled();
+    method @FlaggedApi("com.android.appsearch.flags.enable_list_filter_tokenize_function") public boolean isListFilterTokenizeFunctionEnabled();
     method public boolean isNumericSearchEnabled();
     method public boolean isVerbatimSearchEnabled();
     method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public void writeToParcel(@NonNull android.os.Parcel, int);
     field @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.SearchSpec> CREATOR;
+    field @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+    field @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+    field @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3; // 0x3
     field public static final int GROUPING_TYPE_PER_NAMESPACE = 2; // 0x2
     field public static final int GROUPING_TYPE_PER_PACKAGE = 1; // 0x1
     field @FlaggedApi("com.android.appsearch.flags.enable_grouping_type_per_schema") public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
@@ -510,12 +573,19 @@
     method @FlaggedApi("com.android.appsearch.flags.enable_search_spec_filter_properties") @NonNull public android.app.appsearch.SearchSpec.Builder addFilterPropertyPaths(@NonNull String, @NonNull java.util.Collection<android.app.appsearch.PropertyPath>);
     method @NonNull public android.app.appsearch.SearchSpec.Builder addFilterSchemas(@NonNull java.lang.String...);
     method @NonNull public android.app.appsearch.SearchSpec.Builder addFilterSchemas(@NonNull java.util.Collection<java.lang.String>);
+    method @FlaggedApi("com.android.appsearch.flags.enable_informational_ranking_expressions") @NonNull public android.app.appsearch.SearchSpec.Builder addInformationalRankingExpressions(@NonNull java.lang.String...);
+    method @FlaggedApi("com.android.appsearch.flags.enable_informational_ranking_expressions") @NonNull public android.app.appsearch.SearchSpec.Builder addInformationalRankingExpressions(@NonNull java.util.Collection<java.lang.String>);
     method @NonNull public android.app.appsearch.SearchSpec.Builder addProjection(@NonNull String, @NonNull java.util.Collection<java.lang.String>);
     method @NonNull public android.app.appsearch.SearchSpec.Builder addProjectionPaths(@NonNull String, @NonNull java.util.Collection<android.app.appsearch.PropertyPath>);
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @NonNull public android.app.appsearch.SearchSpec.Builder addSearchEmbeddings(@NonNull android.app.appsearch.EmbeddingVector...);
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @NonNull public android.app.appsearch.SearchSpec.Builder addSearchEmbeddings(@NonNull java.util.Collection<android.app.appsearch.EmbeddingVector>);
     method @NonNull public android.app.appsearch.SearchSpec build();
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @NonNull public android.app.appsearch.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+    method @FlaggedApi("com.android.appsearch.flags.enable_schema_embedding_property_config") @NonNull public android.app.appsearch.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
     method @NonNull public android.app.appsearch.SearchSpec.Builder setJoinSpec(@NonNull android.app.appsearch.JoinSpec);
     method @FlaggedApi("com.android.appsearch.flags.enable_list_filter_has_property_function") @NonNull public android.app.appsearch.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
     method @NonNull public android.app.appsearch.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+    method @FlaggedApi("com.android.appsearch.flags.enable_list_filter_tokenize_function") @NonNull public android.app.appsearch.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
     method @NonNull public android.app.appsearch.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method @NonNull public android.app.appsearch.SearchSpec.Builder setNumericSearchEnabled(boolean);
     method @NonNull public android.app.appsearch.SearchSpec.Builder setOrder(int);
@@ -675,6 +745,57 @@
 
 }
 
+package android.app.appsearch.functions {
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public final class AppFunctionManager {
+    method public void executeAppFunction(@NonNull android.app.appsearch.functions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appsearch.AppSearchResult<android.app.appsearch.functions.ExecuteAppFunctionResponse>>);
+    field public static final String PERMISSION_BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public abstract class AppFunctionService extends android.app.Service {
+    ctor public AppFunctionService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appsearch.functions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appsearch.AppSearchResult<android.app.appsearch.functions.ExecuteAppFunctionResponse>>);
+    field @NonNull public static final String SERVICE_INTERFACE = "android.app.appsearch.functions.AppFunctionService";
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public final class ExecuteAppFunctionRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public String getFunctionIdentifier();
+    method @NonNull public android.app.appsearch.GenericDocument getParameters();
+    method @Nullable public byte[] getSha256Certificate();
+    method @NonNull public String getTargetPackageName();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.functions.ExecuteAppFunctionRequest> CREATOR;
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public static final class ExecuteAppFunctionRequest.Builder {
+    ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String);
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionRequest build();
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionRequest.Builder setSha256Certificate(@Nullable byte[]);
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public final class ExecuteAppFunctionResponse implements android.os.Parcelable {
+    method @FlaggedApi("com.android.appsearch.flags.enable_safe_parcelable_2") public final int describeContents();
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public android.app.appsearch.GenericDocument getResult();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.appsearch.functions.ExecuteAppFunctionResponse> CREATOR;
+    field public static final String PROPERTY_RESULT = "result";
+  }
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public static final class ExecuteAppFunctionResponse.Builder {
+    ctor public ExecuteAppFunctionResponse.Builder();
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionResponse build();
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionResponse.Builder setExtras(@NonNull android.os.Bundle);
+    method @NonNull public android.app.appsearch.functions.ExecuteAppFunctionResponse.Builder setResult(@NonNull android.app.appsearch.GenericDocument);
+  }
+
+}
+
 package android.app.appsearch.observer {
 
   public final class DocumentChangeInfo {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 4a6194e..0114c5d 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -7,3 +7,11 @@
 
 }
 
+package android.app.appsearch.functions {
+
+  @FlaggedApi("com.android.appsearch.flags.enable_app_functions") public final class AppFunctionManager {
+    field public static final String PERMISSION_EXECUTE_APP_FUNCTION = "android.permission.EXECUTE_APP_FUNCTION";
+  }
+
+}
+
diff --git a/framework/java/android/app/appsearch/AppSearchEnvironment.java b/framework/java/android/app/appsearch/AppSearchEnvironment.java
deleted file mode 100644
index 7f8a894..0000000
--- a/framework/java/android/app/appsearch/AppSearchEnvironment.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.appsearch;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.os.UserHandle;
-
-import java.io.File;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-
-/**
- * An interface which exposes environment specific methods for AppSearch.
- * @hide
- */
-public interface AppSearchEnvironment {
-
-  /** Returns the directory to initialize appsearch based on the environment. */
-  public File getAppSearchDir(@NonNull Context context, @NonNull UserHandle userHandle);
-
-  /** Returns the correct context for the user based on the environment. */
-  public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle);
-
-  /** Returns an ExecutorService based on given parameters. */
-  public ExecutorService createExecutorService(
-      int corePoolSize,
-      int maxConcurrency,
-      long keepAliveTime,
-      TimeUnit unit,
-      BlockingQueue<Runnable> workQueue,
-      int priority);
-
-  /** Returns an ExecutorService with a single thread. */
-  public ExecutorService createSingleThreadExecutor();
-
-  /**
-   * Returns a cache directory for creating temporary files like in case of migrating documents.
-   */
-  @Nullable
-  File getCacheDir(@NonNull Context context);
-}
-
diff --git a/framework/java/android/app/appsearch/AppSearchEnvironmentFactory.java b/framework/java/android/app/appsearch/AppSearchEnvironmentFactory.java
index a1d86a1..7db6e88 100644
--- a/framework/java/android/app/appsearch/AppSearchEnvironmentFactory.java
+++ b/framework/java/android/app/appsearch/AppSearchEnvironmentFactory.java
@@ -16,37 +16,41 @@
 
 package android.app.appsearch;
 
+import android.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 /**
  * This is a factory class for implementations needed based on environment for framework code.
+ *
  * @hide
  */
 public class AppSearchEnvironmentFactory {
     private static volatile AppSearchEnvironment mEnvironmentInstance;
 
+    /** Returns the singleton instance of {@link AppSearchEnvironment}. */
+    @NonNull
     public static AppSearchEnvironment getEnvironmentInstance() {
         AppSearchEnvironment localRef = mEnvironmentInstance;
         if (localRef == null) {
             synchronized (AppSearchEnvironmentFactory.class) {
                 localRef = mEnvironmentInstance;
                 if (localRef == null) {
-                    mEnvironmentInstance = localRef =
-                            new FrameworkAppSearchEnvironment();
+                    mEnvironmentInstance = localRef = new FrameworkAppSearchEnvironment();
                 }
             }
         }
         return localRef;
     }
 
+    /** Sets an instance of {@link AppSearchEnvironment}. for testing. */
     @VisibleForTesting
     public static void setEnvironmentInstanceForTest(
-            AppSearchEnvironment appSearchEnvironment) {
+            @NonNull AppSearchEnvironment appSearchEnvironment) {
         synchronized (AppSearchEnvironmentFactory.class) {
             mEnvironmentInstance = appSearchEnvironment;
         }
     }
 
-    private AppSearchEnvironmentFactory() {
-    }
+    private AppSearchEnvironmentFactory() {}
 }
diff --git a/framework/java/android/app/appsearch/AppSearchManager.java b/framework/java/android/app/appsearch/AppSearchManager.java
index 711be92..e0b9473 100644
--- a/framework/java/android/app/appsearch/AppSearchManager.java
+++ b/framework/java/android/app/appsearch/AppSearchManager.java
@@ -22,9 +22,11 @@
 import android.annotation.UserHandleAware;
 import android.app.appsearch.aidl.AppSearchAttributionSource;
 import android.app.appsearch.aidl.IAppSearchManager;
-import android.app.appsearch.flags.Flags;
+import android.app.appsearch.functions.AppFunctionManager;
 import android.content.Context;
+import android.os.Process;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.util.Objects;
@@ -39,11 +41,11 @@
  * <ul>
  *   <li>APIs to index and retrieve data via full-text search.
  *   <li>An API for applications to explicitly grant read-access permission of their data to other
- *   applications.
- *   <b>See: {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}</b>
+ *       applications. <b>See: {@link
+ *       SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}</b>
  *   <li>An API for applications to opt into or out of having their data displayed on System UI
- *   surfaces by the System-designated global querier.
- *   <b>See: {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}</b>
+ *       surfaces by the System-designated global querier. <b>See: {@link
+ *       SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}</b>
  * </ul>
  *
  * <p>Applications create a database by opening an {@link AppSearchSession}.
@@ -78,7 +80,7 @@
  * SetSchemaRequest request = new SetSchemaRequest.Builder().addSchema(emailSchemaType).build();
  * mAppSearchSession.set(request, mExecutor, appSearchResult -&gt; {
  *      if (appSearchResult.isSuccess()) {
- *           //Schema has been successfully set.
+ *           // Schema has been successfully set.
  *      }
  * });</pre>
  *
@@ -105,7 +107,7 @@
  *     .build();
  * mAppSearchSession.put(request, mExecutor, appSearchBatchResult -&gt; {
  *      if (appSearchBatchResult.isSuccess()) {
- *           //All documents have been successfully indexed.
+ *           // All documents have been successfully indexed.
  *      }
  * });</pre>
  *
@@ -125,11 +127,13 @@
 
     private final IAppSearchManager mService;
     private final Context mContext;
+    private final AppFunctionManager mAppFunctionManager;
 
     /** @hide */
     public AppSearchManager(@NonNull Context context, @NonNull IAppSearchManager service) {
         mContext = Objects.requireNonNull(context);
         mService = Objects.requireNonNull(service);
+        mAppFunctionManager = new AppFunctionManager(context, service);
     }
 
     /** Contains information about how to create the search session. */
@@ -211,7 +215,8 @@
                 searchContext,
                 mService,
                 mContext.getUser(),
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(
+                        mContext, /* callingPid= */ Process.myPid()),
                 AppSearchEnvironmentFactory.getEnvironmentInstance().getCacheDir(mContext),
                 executor,
                 callback);
@@ -237,8 +242,10 @@
         GlobalSearchSession.createGlobalSearchSession(
                 mService,
                 mContext.getUser(),
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                executor, callback);
+                AppSearchAttributionSource.createAttributionSource(
+                        mContext, /* callingPid= */ Process.myPid()),
+                executor,
+                callback);
     }
 
     /**
@@ -253,8 +260,8 @@
      * initialization process will create one under the user's credential encrypted directory.
      *
      * @param executor Executor on which to invoke the callback.
-     * @param callback The {@link AppSearchResult}&lt;{@link EnterpriseGlobalSearchSession}&gt;
-     *     of performing this operation. Or a {@link AppSearchResult} with failure reason code and
+     * @param callback The {@link AppSearchResult}&lt;{@link EnterpriseGlobalSearchSession}&gt; of
+     *     performing this operation. Or a {@link AppSearchResult} with failure reason code and
      *     error information.
      */
     @FlaggedApi(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)
@@ -267,8 +274,16 @@
         EnterpriseGlobalSearchSession.createEnterpriseGlobalSearchSession(
                 mService,
                 mContext.getUser(),
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(
+                        mContext, /* callingPid= */ Process.myPid()),
                 executor,
                 callback);
     }
+
+    /** Returns an instance of {@link android.app.appsearch.functions.AppFunctionManager}. */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    @NonNull
+    public AppFunctionManager getAppFunctionManager() {
+        return mAppFunctionManager;
+    }
 }
diff --git a/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java b/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
index 7dc527a..58c9916 100644
--- a/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
+++ b/framework/java/android/app/appsearch/AppSearchManagerFrameworkInitializer.java
@@ -33,12 +33,13 @@
      * Called by {@link SystemServiceRegistry}'s static initializer and registers all AppSearch
      * services to {@link Context}, so that {@link Context#getSystemService} can return them.
      *
-     * @throws IllegalStateException if this is called from anywhere besides
-     *     {@link SystemServiceRegistry}
+     * @throws IllegalStateException if this is called from anywhere besides {@link
+     *     SystemServiceRegistry}
      */
     public static void initialize() {
         SystemServiceRegistry.registerContextAwareService(
-                Context.APP_SEARCH_SERVICE, AppSearchManager.class,
+                Context.APP_SEARCH_SERVICE,
+                AppSearchManager.class,
                 (context, service) ->
                         new AppSearchManager(context, IAppSearchManager.Stub.asInterface(service)));
     }
diff --git a/framework/java/android/app/appsearch/AppSearchMigrationHelper.java b/framework/java/android/app/appsearch/AppSearchMigrationHelper.java
index 2b5967b..11ffe38 100644
--- a/framework/java/android/app/appsearch/AppSearchMigrationHelper.java
+++ b/framework/java/android/app/appsearch/AppSearchMigrationHelper.java
@@ -33,12 +33,14 @@
 import android.app.appsearch.exceptions.AppSearchException;
 import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.stats.SchemaMigrationStats;
+import android.app.appsearch.util.ExceptionUtil;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.ArraySet;
+
 import java.io.Closeable;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -58,8 +60,9 @@
  * The helper class for {@link AppSearchSchema} migration.
  *
  * <p>It will query and migrate {@link GenericDocument} in given type to a new version.
- * Application-specific cache directory is used to store the temporary files created
- * during migration.
+ * Application-specific cache directory is used to store the temporary files created during
+ * migration.
+ *
  * @hide
  */
 public class AppSearchMigrationHelper implements Closeable {
@@ -67,38 +70,43 @@
     private final AppSearchAttributionSource mCallerAttributionSource;
     private final String mDatabaseName;
     private final UserHandle mUserHandle;
-    @Nullable
-    private final File mTempDirectoryForSchemaMigration;
+    @Nullable private final File mTempDirectoryForSchemaMigration;
     private final File mMigratedFile;
     private final Set<String> mDestinationTypes;
     private int mTotalNeedMigratedDocumentCount = 0;
 
     /**
      * Initializes an AppSearchMigrationHelper instance.
+     *
      * @param service The {@link IAppSearchManager} service from which to make api calls.
      * @param userHandle The user for which the session should be created.
      * @param callerAttributionSource The attribution source containing the caller's package name
-     *                                and uid
+     *     and uid
      * @param databaseName The name of the database where this schema lives.
      * @param newSchemas The set of new schemas to update existing schemas.
      * @param tempDirectoryForSchemaMigration The directory to create temporary files needed for
-     *                                        migration. If this is null, the default temporary-file
-     *                                        directory (/data/local/tmp) will be used.
+     *     migration. If this is null, the default temporary-file directory (/data/local/tmp) will
+     *     be used.
      * @throws IOException on failure to create a temporary file.
      */
-    AppSearchMigrationHelper(@NonNull IAppSearchManager service,
+    AppSearchMigrationHelper(
+            @NonNull IAppSearchManager service,
             @NonNull UserHandle userHandle,
             @NonNull AppSearchAttributionSource callerAttributionSource,
             @NonNull String databaseName,
             @NonNull Set<AppSearchSchema> newSchemas,
-            @Nullable File tempDirectoryForSchemaMigration) throws IOException {
+            @Nullable File tempDirectoryForSchemaMigration)
+            throws IOException {
         mService = Objects.requireNonNull(service);
         mUserHandle = Objects.requireNonNull(userHandle);
         mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
         mDatabaseName = Objects.requireNonNull(databaseName);
         mTempDirectoryForSchemaMigration = tempDirectoryForSchemaMigration;
-        mMigratedFile = File.createTempFile(/*prefix=*/ "appsearch",
-                /*suffix=*/ null, mTempDirectoryForSchemaMigration);
+        mMigratedFile =
+                File.createTempFile(
+                        /* prefix= */ "appsearch",
+                        /* suffix= */ null,
+                        mTempDirectoryForSchemaMigration);
         mDestinationTypes = new ArraySet<>(newSchemas.size());
         for (AppSearchSchema newSchema : newSchemas) {
             mDestinationTypes.add(newSchema.getSchemaType());
@@ -106,40 +114,48 @@
     }
 
     /**
-     * Queries all documents that need to be migrated to a different version and transform
-     * documents to that version by passing them to the provided {@link Migrator}.
+     * Queries all documents that need to be migrated to a different version and transform documents
+     * to that version by passing them to the provided {@link Migrator}.
      *
-     * <p>The method will be executed on the executor provided to
-     * {@link AppSearchSession#setSchema}.
+     * <p>The method will be executed on the executor provided to {@link
+     * AppSearchSession#setSchema}.
      *
      * @param schemaType The schema type that needs to be updated and whose {@link GenericDocument}
-     *                   need to be migrated.
-     * @param migrator The {@link Migrator} that will upgrade or downgrade a {@link
-     *     GenericDocument} to new version.
-     * @param schemaMigrationStatsBuilder    The {@link SchemaMigrationStats.Builder} contains
-     *                                       schema migration stats information
+     *     need to be migrated.
+     * @param migrator The {@link Migrator} that will upgrade or downgrade a {@link GenericDocument}
+     *     to new version.
+     * @param schemaMigrationStatsBuilder The {@link SchemaMigrationStats.Builder} contains schema
+     *     migration stats information
      */
     @WorkerThread
-    void queryAndTransform(@NonNull String schemaType, @NonNull Migrator migrator,
-            int currentVersion, int finalVersion,
+    void queryAndTransform(
+            @NonNull String schemaType,
+            @NonNull Migrator migrator,
+            int currentVersion,
+            int finalVersion,
             @Nullable SchemaMigrationStats.Builder schemaMigrationStatsBuilder)
             throws IOException, AppSearchException, InterruptedException, ExecutionException {
-        File queryFile = File.createTempFile(/*prefix=*/ "appsearch",
-                /*suffix=*/ null, mTempDirectoryForSchemaMigration);
+        File queryFile =
+                File.createTempFile(
+                        /* prefix= */ "appsearch",
+                        /* suffix= */ null,
+                        mTempDirectoryForSchemaMigration);
         try (ParcelFileDescriptor fileDescriptor =
-                     ParcelFileDescriptor.open(queryFile, MODE_WRITE_ONLY)) {
+                ParcelFileDescriptor.open(queryFile, MODE_WRITE_ONLY)) {
             CountDownLatch latch = new CountDownLatch(1);
             AtomicReference<AppSearchResult<Void>> resultReference = new AtomicReference<>();
             mService.writeSearchResultsToFile(
-                    new WriteSearchResultsToFileAidlRequest(mCallerAttributionSource, mDatabaseName,
+                    new WriteSearchResultsToFileAidlRequest(
+                            mCallerAttributionSource,
+                            mDatabaseName,
                             fileDescriptor,
-                            /*searchExpression=*/ "",
+                            /* searchExpression= */ "",
                             new SearchSpec.Builder()
                                     .addFilterSchemas(schemaType)
                                     .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                                     .build(),
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
                         public void onResult(AppSearchResultParcel resultParcel) {
@@ -152,27 +168,26 @@
             if (!result.isSuccess()) {
                 throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
             }
-            readAndTransform(queryFile, migrator, currentVersion, finalVersion,
-                    schemaMigrationStatsBuilder);
+            readAndTransform(
+                    queryFile, migrator, currentVersion, finalVersion, schemaMigrationStatsBuilder);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         } finally {
             queryFile.delete();
         }
     }
 
     /**
-     * Puts all {@link GenericDocument} migrated from the previous call to
-     * {@link #queryAndTransform} into AppSearch.
+     * Puts all {@link GenericDocument} migrated from the previous call to {@link
+     * #queryAndTransform} into AppSearch.
      *
-     * <p> This method should be only called once.
+     * <p>This method should be only called once.
      *
      * @param responseBuilder a SetSchemaResponse builder whose result will be returned by this
-     *                        function with any
-     *                        {@link android.app.appsearch.SetSchemaResponse.MigrationFailure}
-     *                        added in.
-     * @param schemaMigrationStatsBuilder    The {@link SchemaMigrationStats.Builder} contains
-     *                                       schema migration stats information
+     *     function with any {@link android.app.appsearch.SetSchemaResponse.MigrationFailure} added
+     *     in.
+     * @param schemaMigrationStatsBuilder The {@link SchemaMigrationStats.Builder} contains schema
+     *     migration stats information
      * @param totalLatencyStartTimeMillis start timestamp to calculate total migration latency in
      *     Millis
      * @return the {@link SetSchemaResponse} for {@link AppSearchSession#setSchema} call.
@@ -186,17 +201,19 @@
             return AppSearchResult.newSuccessfulResult(responseBuilder.build());
         }
         try (ParcelFileDescriptor fileDescriptor =
-                     ParcelFileDescriptor.open(mMigratedFile, MODE_READ_ONLY)) {
+                ParcelFileDescriptor.open(mMigratedFile, MODE_READ_ONLY)) {
             CountDownLatch latch = new CountDownLatch(1);
             AtomicReference<AppSearchResult<List<MigrationFailure>>> resultReference =
                     new AtomicReference<>();
             mService.putDocumentsFromFile(
-                    new PutDocumentsFromFileAidlRequest(mCallerAttributionSource, mDatabaseName,
+                    new PutDocumentsFromFileAidlRequest(
+                            mCallerAttributionSource,
+                            mDatabaseName,
                             fileDescriptor,
                             mUserHandle,
                             schemaMigrationStatsBuilder.build(),
                             totalLatencyStartTimeMillis,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
                         public void onResult(AppSearchResultParcel resultParcel) {
@@ -210,10 +227,10 @@
                 return AppSearchResult.newFailedResult(result);
             }
             List<MigrationFailure> migrationFailures =
-                Objects.requireNonNull(result.getResultValue());
+                    Objects.requireNonNull(result.getResultValue());
             responseBuilder.addMigrationFailures(migrationFailures);
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         } catch (InterruptedException | IOException | RuntimeException e) {
             return AppSearchResult.throwableToFailedResult(e);
         } finally {
@@ -229,13 +246,17 @@
      *
      * <p>Save migrated {@link GenericDocument}s to the {@link #mMigratedFile}.
      */
-    private void readAndTransform(@NonNull File file, @NonNull Migrator migrator,
-            int currentVersion, int finalVersion,
+    private void readAndTransform(
+            @NonNull File file,
+            @NonNull Migrator migrator,
+            int currentVersion,
+            int finalVersion,
             @Nullable SchemaMigrationStats.Builder schemaMigrationStatsBuilder)
             throws IOException, AppSearchException {
         try (DataInputStream inputStream = new DataInputStream(new FileInputStream(file));
-             DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(
-                     mMigratedFile, /*append=*/ true))) {
+                DataOutputStream outputStream =
+                        new DataOutputStream(
+                                new FileOutputStream(mMigratedFile, /* append= */ true))) {
             GenericDocument document;
             while (true) {
                 try {
@@ -278,13 +299,12 @@
      * Reads a {@link GenericDocument} from given {@link DataInputStream}.
      *
      * @param inputStream The inputStream to read from
-     *
-     * @throws IOException        on read failure.
-     * @throws EOFException       if {@link java.io.InputStream} reaches the end.
+     * @throws IOException on read failure.
+     * @throws EOFException if {@link java.io.InputStream} reaches the end.
      */
     @NonNull
-    public static GenericDocument readDocumentFromInputStream(
-            @NonNull DataInputStream inputStream) throws IOException {
+    public static GenericDocument readDocumentFromInputStream(@NonNull DataInputStream inputStream)
+            throws IOException {
         int length = inputStream.readInt();
         if (length == 0) {
             throw new EOFException();
@@ -297,23 +317,21 @@
             parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
             parcel.setDataPosition(0);
             GenericDocumentParcel genericDocumentParcel =
-                    parcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
+                    GenericDocumentParcel.CREATOR.createFromParcel(parcel);
             return new GenericDocument(genericDocumentParcel);
         } finally {
             parcel.recycle();
         }
     }
 
-    /**
-     * Serializes a {@link GenericDocument} and writes into the given {@link DataOutputStream}.
-     */
+    /** Serializes a {@link GenericDocument} and writes into the given {@link DataOutputStream}. */
     public static void writeDocumentToOutputStream(
             @NonNull DataOutputStream outputStream, @NonNull GenericDocument document)
             throws IOException {
         GenericDocumentParcel documentParcel = document.getDocumentParcel();
         Parcel parcel = Parcel.obtain();
         try {
-            parcel.writeParcelable(documentParcel, /*parcelableFlags=*/ 0);
+            documentParcel.writeToParcel(parcel, /* flags= */ 0);
             byte[] serializedMessage = parcel.marshall();
             outputStream.writeInt(serializedMessage.length);
             outputStream.write(serializedMessage);
diff --git a/framework/java/android/app/appsearch/AppSearchSession.java b/framework/java/android/app/appsearch/AppSearchSession.java
index d8ce322..1f0ab7e 100644
--- a/framework/java/android/app/appsearch/AppSearchSession.java
+++ b/framework/java/android/app/appsearch/AppSearchSession.java
@@ -16,6 +16,7 @@
 
 package android.app.appsearch;
 
+import static android.app.appsearch.AppSearchResult.RESULT_INTERNAL_ERROR;
 import static android.app.appsearch.SearchSessionUtil.safeExecute;
 
 import android.annotation.CallbackExecutor;
@@ -25,8 +26,9 @@
 import android.app.appsearch.aidl.AppSearchBatchResultParcel;
 import android.app.appsearch.aidl.AppSearchResultParcel;
 import android.app.appsearch.aidl.DocumentsParcel;
-import android.app.appsearch.aidl.GetSchemaAidlRequest;
+import android.app.appsearch.aidl.GetDocumentsAidlRequest;
 import android.app.appsearch.aidl.GetNamespacesAidlRequest;
+import android.app.appsearch.aidl.GetSchemaAidlRequest;
 import android.app.appsearch.aidl.GetStorageInfoAidlRequest;
 import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
 import android.app.appsearch.aidl.IAppSearchManager;
@@ -35,13 +37,14 @@
 import android.app.appsearch.aidl.PersistToDiskAidlRequest;
 import android.app.appsearch.aidl.PutDocumentsAidlRequest;
 import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
-import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequestCreator;
 import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
+import android.app.appsearch.aidl.ReportUsageAidlRequest;
 import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
 import android.app.appsearch.aidl.SetSchemaAidlRequest;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.stats.SchemaMigrationStats;
+import android.app.appsearch.util.ExceptionUtil;
 import android.app.appsearch.util.SchemaMigrationUtil;
 import android.os.Build;
 import android.os.RemoteException;
@@ -69,8 +72,8 @@
 /**
  * Provides a connection to a single AppSearch database.
  *
- * <p>An {@link AppSearchSession} instance provides access to database operations such as
- * setting a schema, adding documents, and searching.
+ * <p>An {@link AppSearchSession} instance provides access to database operations such as setting a
+ * schema, adding documents, and searching.
  *
  * <p>This class is thread safe.
  *
@@ -83,29 +86,27 @@
     private final String mDatabaseName;
     private final UserHandle mUserHandle;
     private final IAppSearchManager mService;
-    @Nullable
-    private final File mCacheDirectory;
+    @Nullable private final File mCacheDirectory;
 
     private boolean mIsMutated = false;
     private boolean mIsClosed = false;
 
     /**
-     * Creates a search session for the client, defined by the {@code userHandle} and
-     * {@code packageName}.
+     * Creates a search session for the client, defined by the {@code userHandle} and {@code
+     * packageName}.
      *
      * @param searchContext The {@link AppSearchManager.SearchContext} contains all information to
-     *                      create a new {@link AppSearchSession}.
+     *     create a new {@link AppSearchSession}.
      * @param service The {@link IAppSearchManager} service from which to make api calls.
      * @param userHandle The user for which the session should be created.
      * @param callerAttributionSource The attribution source containing the caller's package name
-     *                                and uid.
+     *     and uid.
      * @param cacheDirectory The directory to create temporary files needed for migration. If this
-     *                       is null, the default temporary-file directory (/data/local/tmp) will be
-     *                       used.
+     *     is null, the default temporary-file directory (/data/local/tmp) will be used.
      * @param executor Executor on which to invoke the callback.
      * @param callback The {@link AppSearchResult}&lt;{@link AppSearchSession}&gt; of performing
-     *                 this operation. Or a {@link AppSearchResult} with failure reason code and
-     *                 error information.
+     *     this operation. Or a {@link AppSearchResult} with failure reason code and error
+     *     information.
      */
     static void createSearchSession(
             @NonNull AppSearchManager.SearchContext searchContext,
@@ -116,8 +117,12 @@
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<AppSearchResult<AppSearchSession>> callback) {
         AppSearchSession searchSession =
-                new AppSearchSession(service, userHandle, callerAttributionSource,
-                        searchContext.mDatabaseName, cacheDirectory);
+                new AppSearchSession(
+                        service,
+                        userHandle,
+                        callerAttributionSource,
+                        searchContext.mDatabaseName,
+                        cacheDirectory);
         searchSession.initialize(executor, callback);
     }
 
@@ -131,30 +136,38 @@
                     new InitializeAidlRequest(
                             mCallerAttributionSource,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<Void> result = resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    callback.accept(
-                                            AppSearchResult.newSuccessfulResult(
-                                                    AppSearchSession.this));
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<Void> result = resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            callback.accept(
+                                                    AppSearchResult.newSuccessfulResult(
+                                                            AppSearchSession.this));
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
+                                        }
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
-    private AppSearchSession(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle,
+    private AppSearchSession(
+            @NonNull IAppSearchManager service,
+            @NonNull UserHandle userHandle,
             @NonNull AppSearchAttributionSource callerAttributionSource,
-            @NonNull String databaseName, @Nullable File cacheDirectory) {
+            @NonNull String databaseName,
+            @Nullable File cacheDirectory) {
         mService = service;
         mUserHandle = userHandle;
         mCallerAttributionSource = callerAttributionSource;
@@ -173,10 +186,10 @@
      *
      * @param request the schema to set or update the AppSearch database to.
      * @param workExecutor Executor on which to schedule heavy client-side background work such as
-     *                     transforming documents.
+     *     transforming documents.
      * @param callbackExecutor 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}.
+     *     operation succeeds, the callback will be invoked with {@code null}.
      */
     public void setSchema(
             @NonNull SetSchemaRequest request,
@@ -204,11 +217,7 @@
         // No need to trigger migration if user never set migrator
         if (request.getMigrators().isEmpty()) {
             setSchemaNoMigrations(
-                    request,
-                    schemaList,
-                    visibilityConfigs,
-                    callbackExecutor,
-                    callback);
+                    request, schemaList, visibilityConfigs, callbackExecutor, callback);
         } else {
             setSchemaWithMigrations(
                     request,
@@ -232,8 +241,7 @@
             @NonNull Consumer<AppSearchResult<GetSchemaResponse>> callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
-        String targetPackageName =
-            Objects.requireNonNull(mCallerAttributionSource.getPackageName());
+        String targetPackageName = mCallerAttributionSource.getPackageName();
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         try {
             mService.getSchema(
@@ -242,34 +250,40 @@
                             targetPackageName,
                             mDatabaseName,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                            /*isForEnterprise=*/ false),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
+                            /* isForEnterprise= */ false),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<GetSchemaResponse> result =
-                                        resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    GetSchemaResponse response =
-                                            Objects.requireNonNull(result.getResultValue());
-                                    callback.accept(AppSearchResult.newSuccessfulResult(response));
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<GetSchemaResponse> result =
+                                                resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            GetSchemaResponse response =
+                                                    Objects.requireNonNull(result.getResultValue());
+                                            callback.accept(
+                                                    AppSearchResult.newSuccessfulResult(response));
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
+                                        }
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
     /**
      * Retrieves the set of all namespaces in the current database with at least one document.
      *
-     * @param executor        Executor on which to invoke the callback.
-     * @param callback        Callback to receive the namespaces.
+     * @param executor Executor on which to invoke the callback.
+     * @param callback Callback to receive the namespaces.
      */
     public void getNamespaces(
             @NonNull @CallbackExecutor Executor executor,
@@ -283,25 +297,32 @@
                             mCallerAttributionSource,
                             mDatabaseName,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<List<String>> result = resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    Set<String> namespaces =
-                                            new ArraySet<>(result.getResultValue());
-                                    callback.accept(
-                                            AppSearchResult.newSuccessfulResult(namespaces));
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<List<String>> result =
+                                                resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            Set<String> namespaces =
+                                                    new ArraySet<>(result.getResultValue());
+                                            callback.accept(
+                                                    AppSearchResult.newSuccessfulResult(
+                                                            namespaces));
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
+                                        }
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -314,13 +335,11 @@
      *
      * @param request 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 IDs of the input
-     *                 documents. The values are {@code null} if they were successfully indexed,
-     *                 or a failed {@link AppSearchResult} otherwise. If an unexpected internal
-     *                 error occurs in the AppSearch service,
-     *                 {@link BatchResultCallback#onSystemError} will be invoked with a
-     *                 {@link Throwable}.
+     * @param callback Callback to receive pending result of performing this operation. The keys of
+     *     the returned {@link AppSearchBatchResult} are the IDs of the input documents. The values
+     *     are {@code null} if they were successfully indexed, or a failed {@link AppSearchResult}
+     *     otherwise. If an unexpected internal error occurs in the AppSearch service, {@link
+     *     BatchResultCallback#onSystemError} will be invoked with a {@link Throwable}.
      */
     public void put(
             @NonNull PutDocumentsRequest request,
@@ -330,17 +349,21 @@
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        DocumentsParcel documentsParcel = new DocumentsParcel(
-                toGenericDocumentParcels(request.getGenericDocuments()),
-                toGenericDocumentParcels(request.getTakenActionGenericDocuments()));
+        DocumentsParcel documentsParcel =
+                new DocumentsParcel(
+                        toGenericDocumentParcels(request.getGenericDocuments()),
+                        toGenericDocumentParcels(request.getTakenActionGenericDocuments()));
         try {
             mService.putDocuments(
                     new PutDocumentsAidlRequest(
-                            mCallerAttributionSource, mDatabaseName, documentsParcel,
+                            mCallerAttributionSource,
+                            mDatabaseName,
+                            documentsParcel,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchBatchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchBatchResultParcel resultParcel) {
                             safeExecute(
                                     executor,
@@ -353,13 +376,14 @@
                             safeExecute(
                                     executor,
                                     callback,
-                                    () -> SearchSessionUtil.sendSystemErrorToCallback(
-                                            resultParcel.getResult(), callback));
+                                    () ->
+                                            SearchSessionUtil.sendSystemErrorToCallback(
+                                                    resultParcel.getResult(), callback));
                         }
                     });
             mIsMutated = true;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -370,13 +394,12 @@
      * @param request a request containing a namespace and IDs to get documents for.
      * @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 IDs. The values
-     *                 are the returned {@link GenericDocument}s on success, or a failed
-     *                 {@link AppSearchResult} otherwise. IDs that are not found will return a
-     *                 failed {@link AppSearchResult} with a result code of
-     *                 {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
-     *                 occurs in the AppSearch service, {@link BatchResultCallback#onSystemError}
-     *                 will be invoked with a {@link Throwable}.
+     *     of the returned {@link AppSearchBatchResult} are the input IDs. The values are the
+     *     returned {@link GenericDocument}s on success, or a failed {@link AppSearchResult}
+     *     otherwise. IDs that are not found will return a failed {@link AppSearchResult} with a
+     *     result code of {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
+     *     occurs in the AppSearch service, {@link BatchResultCallback#onSystemError} will be
+     *     invoked with a {@link Throwable}.
      */
     public void getByDocumentId(
             @NonNull GetByDocumentIdRequest request,
@@ -385,29 +408,27 @@
         Objects.requireNonNull(request);
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
-        String targetPackageName =
-            Objects.requireNonNull(mCallerAttributionSource.getPackageName());
+        String targetPackageName = mCallerAttributionSource.getPackageName();
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         try {
             mService.getDocuments(
-                    mCallerAttributionSource,
-                    targetPackageName,
-                    mDatabaseName,
-                    request.getNamespace(),
-                    new ArrayList<>(request.getIds()),
-                    request.getProjectionsInternal(),
-                    mUserHandle,
-                    /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                    /*isForEnterprise=*/ false,
+                    new GetDocumentsAidlRequest(
+                            mCallerAttributionSource,
+                            targetPackageName,
+                            mDatabaseName,
+                            request,
+                            mUserHandle,
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
+                            /* isForEnterprise= */ false),
                     SearchSessionUtil.createGetDocumentCallback(executor, callback));
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
     /**
-     * Retrieves documents from the open {@link AppSearchSession} that match a given query
-     * string and type of search provided.
+     * Retrieves documents from the open {@link AppSearchSession} that match a given query string
+     * and type of search provided.
      *
      * <p>Query strings can be empty, contain one term with no operators, or contain multiple terms
      * and operators.
@@ -453,47 +474,48 @@
      *       the "subject" property.
      * </ul>
      *
-     * <p>The above description covers the basic query operators. Additional advanced query
-     * operator features should be explicitly enabled in the SearchSpec and are described below.
+     * <p>The above description covers the basic query operators. Additional advanced query operator
+     * features should be explicitly enabled in the SearchSpec and are described below.
      *
      * <p>LIST_FILTER_QUERY_LANGUAGE: This feature covers the expansion of the query language to
      * conform to the definition of the list filters language (https://aip.dev/160). This includes:
+     *
      * <ul>
-     *     <li>addition of explicit 'AND' and 'NOT' operators</li>
-     *     <li>property restricts are allowed with groupings (ex. "prop:(a OR b)")</li>
-     *     <li>addition of custom functions to control matching</li>
+     *   <li>addition of explicit 'AND' and 'NOT' operators
+     *   <li>property restricts are allowed with groupings (ex. "prop:(a OR b)")
+     *   <li>addition of custom functions to control matching
      * </ul>
      *
      * <p>The newly added custom functions covered by this feature are:
+     *
      * <ul>
-     *     <li>createList(String...)</li>
-     *     <li>search(String, List<String>)</li>
-     *     <li>propertyDefined(String)</li>
+     *   <li>createList(String...)
+     *   <li>search(String, List&lt;String&gt;)
+     *   <li>propertyDefined(String)
      * </ul>
      *
-     * <p>createList takes a variable number of strings and returns a list of strings.
-     * It is for use with search.
+     * <p>createList takes a variable number of strings and returns a list of strings. It is for use
+     * with search.
      *
-     * <p>search takes a query string that will be parsed according to the supported
-     * query language and an optional list of strings that specify the properties to be
-     * restricted to. This exists as a convenience for multiple property restricts. So,
-     * for example, the query `(subject:foo OR body:foo) (subject:bar OR body:bar)`
-     * could be rewritten as `search("foo bar", createList("subject", "bar"))`.
+     * <p>search takes a query string that will be parsed according to the supported query language
+     * and an optional list of strings that specify the properties to be restricted to. This exists
+     * as a convenience for multiple property restricts. So, for example, the query `(subject:foo OR
+     * body:foo) (subject:bar OR body:bar)` could be rewritten as `search("foo bar",
+     * createList("subject", "bar"))`.
      *
      * <p>propertyDefined takes a string specifying the property of interest and matches all
-     * documents of any type that defines the specified property
-     * (ex. `propertyDefined("sender.name")`). Note that propertyDefined will match so long as
-     * the document's type defines the specified property. It does NOT require that the document
+     * documents of any type that defines the specified property (ex.
+     * `propertyDefined("sender.name")`). Note that propertyDefined will match so long as the
+     * document's type defines the specified property. It does NOT require that the document
      * actually hold any values for this property.
      *
-     * <p>NUMERIC_SEARCH: This feature covers numeric search expressions. In the query language,
-     * the values of properties that have
-     * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} set can be matched with a
-     * numeric search expression (the property, a supported comparator and an integer value).
-     * Supported comparators are <, <=, ==, >= and >.
+     * <p>NUMERIC_SEARCH: This feature covers numeric search expressions. In the query language, the
+     * values of properties that have {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}
+     * set can be matched with a numeric search expression (the property, a supported comparator and
+     * an integer value). Supported comparators are <, <=, ==, >= and >.
      *
-     * <p>Ex. `price < 10` will match all documents that has a numeric value in its price
-     * property that is less than 10.
+     * <p>Ex. `price < 10` will match all documents that has a numeric value in its price property
+     * that is less than 10.
      *
      * <p>VERBATIM_SEARCH: This feature covers the verbatim string operator (quotation marks).
      *
@@ -515,68 +537,78 @@
         Objects.requireNonNull(queryExpression);
         Objects.requireNonNull(searchSpec);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
-        return new SearchResults(mService, mCallerAttributionSource, mDatabaseName, queryExpression,
-                searchSpec, mUserHandle, /*isForEnterprise=*/ false);
+        return new SearchResults(
+                mService,
+                mCallerAttributionSource,
+                mDatabaseName,
+                queryExpression,
+                searchSpec,
+                mUserHandle,
+                /* isForEnterprise= */ false);
     }
 
     /**
-     * Retrieves suggested Strings that could be used as {@code queryExpression} in
-     * {@link #search(String, SearchSpec)} API.
+     * Retrieves suggested Strings that could be used as {@code queryExpression} in {@link
+     * #search(String, SearchSpec)} API.
      *
      * <p>The {@code suggestionQueryExpression} can contain one term with no operators, or contain
      * multiple terms and operators. Operators will be considered as a normal term. Please see the
      * operator examples below. The {@code suggestionQueryExpression} must end with a valid term,
-     * the suggestions are generated based on the last term. If the input
-     * {@code suggestionQueryExpression} doesn't have a valid token, AppSearch will return an
-     * empty result list. Please see the invalid examples below.
+     * the suggestions are generated based on the last term. If the input {@code
+     * suggestionQueryExpression} doesn't have a valid token, AppSearch will return an empty result
+     * list. Please see the invalid examples below.
      *
      * <p>Example: if there are following documents with content stored in AppSearch.
+     *
      * <ul>
-     *     <li>document1: "term1"
-     *     <li>document2: "term1 term2"
-     *     <li>document3: "term1 term2 term3"
-     *     <li>document4: "org"
+     *   <li>document1: "term1"
+     *   <li>document2: "term1 term2"
+     *   <li>document3: "term1 term2 term3"
+     *   <li>document4: "org"
      * </ul>
      *
      * <p>Search suggestions with the single term {@code suggestionQueryExpression} "t", the
      * suggested results are:
+     *
      * <ul>
-     *     <li>"term1" - Use it to be queryExpression in {@link #search} could get 3
-     *     {@link SearchResult}s, which contains document 1, 2 and 3.
-     *     <li>"term2" - Use it to be queryExpression in {@link #search} could get 2
-     *     {@link SearchResult}s, which contains document 2 and 3.
-     *     <li>"term3" - Use it to be queryExpression in {@link #search} could get 1
-     *     {@link SearchResult}, which contains document 3.
+     *   <li>"term1" - Use it to be queryExpression in {@link #search} could get 3 {@link
+     *       SearchResult}s, which contains document 1, 2 and 3.
+     *   <li>"term2" - Use it to be queryExpression in {@link #search} could get 2 {@link
+     *       SearchResult}s, which contains document 2 and 3.
+     *   <li>"term3" - Use it to be queryExpression in {@link #search} could get 1 {@link
+     *       SearchResult}, which contains document 3.
      * </ul>
      *
      * <p>Search suggestions with the multiple term {@code suggestionQueryExpression} "org t", the
-     * suggested result will be "org term1" - The last token is completed by the suggested
-     * String.
+     * suggested result will be "org term1" - The last token is completed by the suggested String.
      *
      * <p>Operators in {@link #search} are supported.
+     *
      * <p><b>NOTE:</b> Exclusion and Grouped Terms in the last term is not supported.
-     * <p>example: "apple -f": This Api will throw an
-     * {@link android.app.appsearch.exceptions.AppSearchException} with
-     * {@link AppSearchResult#RESULT_INVALID_ARGUMENT}.
+     *
+     * <p>example: "apple -f": This Api will throw an {@link
+     * android.app.appsearch.exceptions.AppSearchException} with {@link
+     * AppSearchResult#RESULT_INVALID_ARGUMENT}.
+     *
      * <p>example: "apple (f)": This Api will return an empty results.
      *
-     * <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid
-     * last token, AppSearch will return an empty result list.
+     * <p>Invalid example: All these input {@code suggestionQueryExpression} don't have a valid last
+     * token, AppSearch will return an empty result list.
+     *
      * <ul>
-     *     <li>""      - Empty {@code suggestionQueryExpression}.
-     *     <li>"(f)"   - Ending in a closed brackets.
-     *     <li>"f:"    - Ending in an operator.
-     *     <li>"f    " - Ending in trailing space.
+     *   <li>"" - Empty {@code suggestionQueryExpression}.
+     *   <li>"(f)" - Ending in a closed brackets.
+     *   <li>"f:" - Ending in an operator.
+     *   <li>"f " - Ending in trailing space.
      * </ul>
      *
      * @param suggestionQueryExpression the non empty query string to search suggestions
-     * @param searchSuggestionSpec      spec for setting document filters
+     * @param searchSuggestionSpec spec for setting document filters
      * @param executor Executor on which to invoke the callback.
-     * @param callback Callback to receive the pending result of performing this operation, which
-     *                 is a List of {@link SearchSuggestionResult} on success. The returned
-     *                 suggestion Strings are ordered by the number of {@link SearchResult} you
-     *                 could get by using that suggestion in {@link #search}.
-     *
+     * @param callback Callback to receive the pending result of performing this operation, which is
+     *     a List of {@link SearchSuggestionResult} on success. The returned suggestion Strings are
+     *     ordered by the number of {@link SearchResult} you could get by using that suggestion in
+     *     {@link #search}.
      * @see #search(String, SearchSpec)
      */
     public void searchSuggestion(
@@ -597,30 +629,36 @@
                             suggestionQueryExpression,
                             searchSuggestionSpec,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                try {
-                                    AppSearchResult<List<SearchSuggestionResult>> result =
-                                            resultParcel.getResult();
-                                    if (result.isSuccess()) {
-                                        callback.accept(result);
-                                    } else {
-                                        // TODO(b/261897334) save SDK errors/crashes and send to
-                                        //  server for logging.
-                                        callback.accept(AppSearchResult.newFailedResult(result));
-                                    }
-                                } catch (Exception e) {
-                                    callback.accept(AppSearchResult.throwableToFailedResult(e));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        try {
+                                            AppSearchResult<List<SearchSuggestionResult>> result =
+                                                    resultParcel.getResult();
+                                            if (result.isSuccess()) {
+                                                callback.accept(result);
+                                            } else {
+                                                // TODO(b/261897334) save SDK errors/crashes and
+                                                // send to
+                                                //  server for logging.
+                                                callback.accept(
+                                                        AppSearchResult.newFailedResult(result));
+                                            }
+                                        } catch (Exception e) {
+                                            callback.accept(
+                                                    AppSearchResult.throwableToFailedResult(e));
+                                        }
+                                    });
                         }
-                    }
-            );
+                    });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -639,7 +677,7 @@
      * @param request The usage reporting request.
      * @param executor Executor on which to invoke the callback.
      * @param callback Callback to receive errors. If the operation succeeds, the callback will be
-     *                 invoked with {@code null}.
+     *     invoked with {@code null}.
      */
     public void reportUsage(
             @NonNull ReportUsageRequest request,
@@ -648,22 +686,21 @@
         Objects.requireNonNull(request);
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
-        String targetPackageName =
-            Objects.requireNonNull(mCallerAttributionSource.getPackageName());
+        String targetPackageName = mCallerAttributionSource.getPackageName();
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         try {
             mService.reportUsage(
-                    mCallerAttributionSource,
-                    targetPackageName,
-                    mDatabaseName,
-                    request.getNamespace(),
-                    request.getDocumentId(),
-                    request.getUsageTimestampMillis(),
-                    /*systemUsage=*/ false,
-                    mUserHandle,
-                    /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+                    new ReportUsageAidlRequest(
+                            mCallerAttributionSource,
+                            targetPackageName,
+                            mDatabaseName,
+                            request,
+                            /* systemUsage= */ false,
+                            mUserHandle,
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
                             safeExecute(
                                     executor,
@@ -673,7 +710,7 @@
                     });
             mIsMutated = true;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -691,13 +728,12 @@
      *     index.
      * @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 document IDs. The
-     *                 values are {@code null} on success, or a failed {@link AppSearchResult}
-     *                 otherwise. IDs that are not found will return a failed
-     *                 {@link AppSearchResult} with a result code of
-     *                 {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
-     *                 occurs in the AppSearch service, {@link BatchResultCallback#onSystemError}
-     *                 will be invoked with a {@link Throwable}.
+     *     of the returned {@link AppSearchBatchResult} are the input document IDs. The values are
+     *     {@code null} on success, or a failed {@link AppSearchResult} otherwise. IDs that are not
+     *     found will return a failed {@link AppSearchResult} with a result code of {@link
+     *     AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error occurs in the
+     *     AppSearch service, {@link BatchResultCallback#onSystemError} will be invoked with a
+     *     {@link Throwable}.
      */
     public void remove(
             @NonNull RemoveByDocumentIdRequest request,
@@ -712,12 +748,12 @@
                     new RemoveByDocumentIdAidlRequest(
                             mCallerAttributionSource,
                             mDatabaseName,
-                            request.getNamespace(),
-                            new ArrayList<>(request.getIds()),
+                            request,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchBatchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchBatchResultParcel resultParcel) {
                             safeExecute(
                                     executor,
@@ -726,17 +762,19 @@
                         }
 
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onSystemError(AppSearchResultParcel resultParcel) {
                             safeExecute(
                                     executor,
                                     callback,
-                                    () -> SearchSessionUtil.sendSystemErrorToCallback(
-                                            resultParcel.getResult(), callback));
+                                    () ->
+                                            SearchSessionUtil.sendSystemErrorToCallback(
+                                                    resultParcel.getResult(), callback));
                         }
                     });
             mIsMutated = true;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -754,10 +792,9 @@
      * @param searchSpec Spec containing schemaTypes, namespaces and query expression indicates how
      *     document will be removed. All specific about how to scoring, ordering, snippeting and
      *     resulting will be ignored.
-     * @param executor        Executor on which to invoke the callback.
-     * @param callback        Callback to receive errors resulting from removing the documents. If
-     *                        the operation succeeds, the callback will be invoked with
-     *                        {@code null}.
+     * @param executor Executor on which to invoke the callback.
+     * @param callback Callback to receive errors resulting from removing the documents. If the
+     *     operation succeeds, the callback will be invoked with {@code null}.
      */
     public void remove(
             @NonNull String queryExpression,
@@ -770,8 +807,8 @@
         Objects.requireNonNull(callback);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
         if (searchSpec.getJoinSpec() != null) {
-            throw new IllegalArgumentException("JoinSpec not allowed in removeByQuery, but "
-                    + "JoinSpec was provided.");
+            throw new IllegalArgumentException(
+                    "JoinSpec not allowed in removeByQuery, but " + "JoinSpec was provided.");
         }
         try {
             mService.removeByQuery(
@@ -781,9 +818,10 @@
                             queryExpression,
                             searchSpec,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
                             safeExecute(
                                     executor,
@@ -793,7 +831,7 @@
                     });
             mIsMutated = true;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -803,8 +841,8 @@
      * <p>This may take time proportional to the number of documents and may be inefficient to call
      * repeatedly.
      *
-     * @param executor        Executor on which to invoke the callback.
-     * @param callback        Callback to receive the storage info.
+     * @param executor Executor on which to invoke the callback.
+     * @param callback Callback to receive the storage info.
      */
     public void getStorageInfo(
             @NonNull @CallbackExecutor Executor executor,
@@ -818,29 +856,36 @@
                             mCallerAttributionSource,
                             mDatabaseName,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<StorageInfo> result = resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    callback.accept(AppSearchResult.newSuccessfulResult(
-                                            result.getResultValue()));
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<StorageInfo> result =
+                                                resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            callback.accept(
+                                                    AppSearchResult.newSuccessfulResult(
+                                                            result.getResultValue()));
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
+                                        }
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
     /**
-     * Closes the {@link AppSearchSession} to persist all schema and document updates,
-     * additions, and deletes to disk.
+     * Closes the {@link AppSearchSession} to persist all schema and document updates, additions,
+     * and deletes to disk.
      */
     @Override
     public void close() {
@@ -850,7 +895,7 @@
                         new PersistToDiskAidlRequest(
                                 mCallerAttributionSource,
                                 mUserHandle,
-                                /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()));
+                                /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()));
                 mIsClosed = true;
             } catch (RemoteException e) {
                 Log.e(TAG, "Unable to close the AppSearchSession", e);
@@ -871,52 +916,86 @@
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
         try {
-            SetSchemaAidlRequest setSchemaAidlRequest = new SetSchemaAidlRequest(
-                    mCallerAttributionSource,
-                    mDatabaseName,
-                    schemas,
-                    visibilityConfigs,
-                    request.isForceOverride(),
-                    request.getVersion(),
-                    mUserHandle,
-                    /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                    SchemaMigrationStats.NO_MIGRATION);
+            SetSchemaAidlRequest setSchemaAidlRequest =
+                    new SetSchemaAidlRequest(
+                            mCallerAttributionSource,
+                            mDatabaseName,
+                            schemas,
+                            visibilityConfigs,
+                            request.isForceOverride(),
+                            request.getVersion(),
+                            mUserHandle,
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
+                            SchemaMigrationStats.NO_MIGRATION);
             mService.setSchema(
                     setSchemaAidlRequest,
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<InternalSetSchemaResponse> result =
-                                        resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    try {
-                                        InternalSetSchemaResponse internalSetSchemaResponse =
-                                                result.getResultValue();
-                                        if (!internalSetSchemaResponse.isSuccess()) {
-                                            // check is the set schema call failed because
-                                            // incompatible changes. That's the only case we
-                                            // swallowed in the AppSearchImpl#setSchema().
-                                            callback.accept(AppSearchResult.newFailedResult(
-                                                    AppSearchResult.RESULT_INVALID_SCHEMA,
-                                                    internalSetSchemaResponse.getErrorMessage()));
-                                            return;
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<InternalSetSchemaResponse> result =
+                                                resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            try {
+                                                InternalSetSchemaResponse
+                                                        internalSetSchemaResponse =
+                                                                result.getResultValue();
+                                                if (internalSetSchemaResponse == null) {
+                                                    // Ideally internalSetSchemaResponse should
+                                                    // always be non-null as result is success. In
+                                                    // other cases we directly put result in
+                                                    // AppSearchResult.newSuccessfulResult which
+                                                    // accepts a Nullable value, here we need to
+                                                    // get response by
+                                                    // internalSetSchemaResponse
+                                                    // .getSetSchemaResponse().
+                                                    callback.accept(
+                                                            AppSearchResult.newFailedResult(
+                                                                    RESULT_INTERNAL_ERROR,
+                                                                    "Received null"
+                                                                            + " InternalSetSchema"
+                                                                            + "Response"
+                                                                            + " during setSchema"
+                                                                            + " call"));
+                                                    return;
+                                                }
+                                                if (!internalSetSchemaResponse.isSuccess()) {
+                                                    // check is the set schema call failed
+                                                    // because incompatible changes. That's the only
+                                                    // case we swallowed in the
+                                                    // AppSearchImpl#setSchema().
+                                                    callback.accept(
+                                                            AppSearchResult.newFailedResult(
+                                                                    AppSearchResult
+                                                                            .RESULT_INVALID_SCHEMA,
+                                                                    internalSetSchemaResponse
+                                                                            .getErrorMessage()));
+                                                    return;
+                                                }
+                                                callback.accept(
+                                                        AppSearchResult.newSuccessfulResult(
+                                                                internalSetSchemaResponse
+                                                                        .getSetSchemaResponse()));
+                                            } catch (RuntimeException e) {
+                                                // TODO(b/261897334) save SDK errors/crashes and
+                                                // send to
+                                                //  server for logging.
+                                                callback.accept(
+                                                        AppSearchResult.throwableToFailedResult(e));
+                                            }
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
                                         }
-                                        callback.accept(AppSearchResult.newSuccessfulResult(
-                                                internalSetSchemaResponse.getSetSchemaResponse()));
-                                    } catch (RuntimeException e) {
-                                        // TODO(b/261897334) save SDK errors/crashes and send to
-                                        //  server for logging.
-                                        callback.accept(AppSearchResult.throwableToFailedResult(e));
-                                    }
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -936,219 +1015,302 @@
             @NonNull Consumer<AppSearchResult<SetSchemaResponse>> callback) {
         long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
         long waitExecutorStartLatencyMillis = SystemClock.elapsedRealtime();
-        safeExecute(workExecutor, callback, () -> {
-            try {
-                long waitExecutorEndLatencyMillis = SystemClock.elapsedRealtime();
-                SchemaMigrationStats.Builder statsBuilder = new SchemaMigrationStats.Builder(
-                        mCallerAttributionSource.getPackageName(), mDatabaseName);
+        safeExecute(
+                workExecutor,
+                callback,
+                () -> {
+                    try {
+                        long waitExecutorEndLatencyMillis = SystemClock.elapsedRealtime();
+                        String packageName = mCallerAttributionSource.getPackageName();
+                        SchemaMigrationStats.Builder statsBuilder =
+                                new SchemaMigrationStats.Builder(packageName, mDatabaseName);
 
-                // Migration process
-                // 1. Validate and retrieve all active migrators.
-                long getSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime();
-                CountDownLatch getSchemaLatch = new CountDownLatch(1);
-                AtomicReference<AppSearchResult<GetSchemaResponse>> getSchemaResultRef =
-                        new AtomicReference<>();
-                getSchema(callbackExecutor, (result) -> {
-                    getSchemaResultRef.set(result);
-                    getSchemaLatch.countDown();
-                });
-                getSchemaLatch.await();
-                AppSearchResult<GetSchemaResponse> getSchemaResult = getSchemaResultRef.get();
-                if (!getSchemaResult.isSuccess()) {
-                    // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
-                    safeExecute(
-                            callbackExecutor,
-                            callback,
-                            () -> callback.accept(
-                                    AppSearchResult.newFailedResult(getSchemaResult)));
-                    return;
-                }
-                GetSchemaResponse getSchemaResponse =
-                        Objects.requireNonNull(getSchemaResult.getResultValue());
-                int currentVersion = getSchemaResponse.getVersion();
-                int finalVersion = request.getVersion();
-                Map<String, Migrator> activeMigrators = SchemaMigrationUtil.getActiveMigrators(
-                        getSchemaResponse.getSchemas(), request.getMigrators(), currentVersion,
-                        finalVersion);
-                long getSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
+                        // Migration process
+                        // 1. Validate and retrieve all active migrators.
+                        long getSchemaLatencyStartTimeMillis = SystemClock.elapsedRealtime();
+                        CountDownLatch getSchemaLatch = new CountDownLatch(1);
+                        AtomicReference<AppSearchResult<GetSchemaResponse>> getSchemaResultRef =
+                                new AtomicReference<>();
+                        getSchema(
+                                callbackExecutor,
+                                (result) -> {
+                                    getSchemaResultRef.set(result);
+                                    getSchemaLatch.countDown();
+                                });
+                        getSchemaLatch.await();
+                        AppSearchResult<GetSchemaResponse> getSchemaResult =
+                                getSchemaResultRef.get();
+                        if (!getSchemaResult.isSuccess()) {
+                            // TODO(b/261897334) save SDK errors/crashes and send to server for
+                            // logging.
+                            safeExecute(
+                                    callbackExecutor,
+                                    callback,
+                                    () ->
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(
+                                                            getSchemaResult)));
+                            return;
+                        }
+                        GetSchemaResponse getSchemaResponse =
+                                Objects.requireNonNull(getSchemaResult.getResultValue());
+                        int currentVersion = getSchemaResponse.getVersion();
+                        int finalVersion = request.getVersion();
+                        Map<String, Migrator> activeMigrators =
+                                SchemaMigrationUtil.getActiveMigrators(
+                                        getSchemaResponse.getSchemas(),
+                                        request.getMigrators(),
+                                        currentVersion,
+                                        finalVersion);
+                        long getSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
 
-                // No need to trigger migration if no migrator is active.
-                if (activeMigrators.isEmpty()) {
-                    setSchemaNoMigrations(request, schemas, visibilityConfigs,
-                            callbackExecutor, callback);
-                    return;
-                }
+                        // No need to trigger migration if no migrator is active.
+                        if (activeMigrators.isEmpty()) {
+                            setSchemaNoMigrations(
+                                    request,
+                                    schemas,
+                                    visibilityConfigs,
+                                    callbackExecutor,
+                                    callback);
+                            return;
+                        }
 
-                // 2. SetSchema with forceOverride=false, to retrieve the list of
-                // incompatible/deleted types.
-                long firstSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
-                CountDownLatch setSchemaLatch = new CountDownLatch(1);
-                AtomicReference<AppSearchResult<InternalSetSchemaResponse>> setSchemaResultRef =
-                        new AtomicReference<>();
-
-                SetSchemaAidlRequest setSchemaAidlRequest = new SetSchemaAidlRequest(
-                        mCallerAttributionSource,
-                        mDatabaseName,
-                        schemas,
-                        visibilityConfigs,
-                        /*forceOverride=*/ false,
-                        request.getVersion(),
-                        mUserHandle,
-                        /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                        SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE);
-                mService.setSchema(
-                        setSchemaAidlRequest,
-                        new IAppSearchResultCallback.Stub() {
-                            @Override
-                            public void onResult(AppSearchResultParcel resultParcel) {
-                                setSchemaResultRef.set(resultParcel.getResult());
-                                setSchemaLatch.countDown();
-                            }
-                        });
-                setSchemaLatch.await();
-                AppSearchResult<InternalSetSchemaResponse> setSchemaResult =
-                        setSchemaResultRef.get();
-                if (!setSchemaResult.isSuccess()) {
-                    // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
-                    safeExecute(
-                            callbackExecutor,
-                            callback,
-                            () -> callback.accept(
-                                    AppSearchResult.newFailedResult(setSchemaResult)));
-                    return;
-                }
-                InternalSetSchemaResponse internalSetSchemaResponse1 =
-                        setSchemaResult.getResultValue();
-                long firstSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
-
-                // 3. If forceOverride is false, check that all incompatible types will be migrated.
-                // If some aren't we must throw an error, rather than proceeding and deleting those
-                // types.
-                SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(
-                        internalSetSchemaResponse1, activeMigrators.keySet());
-
-                try (AppSearchMigrationHelper migrationHelper = new AppSearchMigrationHelper(
-                        mService, mUserHandle, mCallerAttributionSource, mDatabaseName,
-                        request.getSchemas(), mCacheDirectory)) {
-
-                    // 4. Trigger migration for all migrators.
-                    long queryAndTransformLatencyStartTimeMillis = SystemClock.elapsedRealtime();
-                    for (Map.Entry<String, Migrator> entry : activeMigrators.entrySet()) {
-                        migrationHelper.queryAndTransform(/*schemaType=*/ entry.getKey(),
-                                /*migrator=*/ entry.getValue(), currentVersion,
-                                finalVersion, statsBuilder);
-                    }
-                    long queryAndTransformLatencyEndTimeMillis = SystemClock.elapsedRealtime();
-
-                    // 5. SetSchema a second time with forceOverride=true if the first attempted
-                    // failed.
-                    long secondSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
-                    InternalSetSchemaResponse internalSetSchemaResponse;
-                    if (internalSetSchemaResponse1.isSuccess()) {
-                        internalSetSchemaResponse = internalSetSchemaResponse1;
-                    } else {
-                        CountDownLatch setSchema2Latch = new CountDownLatch(1);
+                        // 2. SetSchema with forceOverride=false, to retrieve the list of
+                        // incompatible/deleted types.
+                        long firstSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
+                        CountDownLatch setSchemaLatch = new CountDownLatch(1);
                         AtomicReference<AppSearchResult<InternalSetSchemaResponse>>
-                                setSchema2ResultRef = new AtomicReference<>();
-                        // only trigger second setSchema() call if the first one is fail.
-                        SetSchemaAidlRequest setSchemaAidlRequest1 = new SetSchemaAidlRequest(
-                                mCallerAttributionSource,
-                                mDatabaseName,
-                                schemas,
-                                visibilityConfigs,
-                                /*forceOverride=*/ true,
-                                request.getVersion(),
-                                mUserHandle,
-                                /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                                SchemaMigrationStats.SECOND_CALL_APPLY_NEW_SCHEMA);
+                                setSchemaResultRef = new AtomicReference<>();
+
+                        SetSchemaAidlRequest setSchemaAidlRequest =
+                                new SetSchemaAidlRequest(
+                                        mCallerAttributionSource,
+                                        mDatabaseName,
+                                        schemas,
+                                        visibilityConfigs,
+                                        /* forceOverride= */ false,
+                                        request.getVersion(),
+                                        mUserHandle,
+                                        /* binderCallStartTimeMillis= */ SystemClock
+                                                .elapsedRealtime(),
+                                        SchemaMigrationStats.FIRST_CALL_GET_INCOMPATIBLE);
                         mService.setSchema(
-                                setSchemaAidlRequest1,
+                                setSchemaAidlRequest,
                                 new IAppSearchResultCallback.Stub() {
                                     @Override
+                                    @SuppressWarnings({"rawtypes", "unchecked"})
                                     public void onResult(AppSearchResultParcel resultParcel) {
-                                        setSchema2ResultRef.set(resultParcel.getResult());
-                                        setSchema2Latch.countDown();
+                                        setSchemaResultRef.set(resultParcel.getResult());
+                                        setSchemaLatch.countDown();
                                     }
                                 });
-                        setSchema2Latch.await();
-                        AppSearchResult<InternalSetSchemaResponse> setSchema2Result =
-                                setSchema2ResultRef.get();
-                        if (!setSchema2Result.isSuccess()) {
-                            // we failed to set the schema in second time with forceOverride = true,
-                            // which is an impossible case. Since we only swallow the incompatible
-                            // error in the first setSchema call, all other errors will be thrown at
-                            // the first time.
+                        setSchemaLatch.await();
+                        AppSearchResult<InternalSetSchemaResponse> setSchemaResult =
+                                setSchemaResultRef.get();
+                        if (!setSchemaResult.isSuccess()) {
                             // TODO(b/261897334) save SDK errors/crashes and send to server for
-                            //  logging.
+                            // logging.
                             safeExecute(
                                     callbackExecutor,
                                     callback,
-                                    () -> callback.accept(
-                                            AppSearchResult.newFailedResult(setSchema2Result)));
+                                    () ->
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(
+                                                            setSchemaResult)));
                             return;
                         }
-                        InternalSetSchemaResponse internalSetSchemaResponse2 =
-                                setSchema2Result.getResultValue();
-                        if (!internalSetSchemaResponse2.isSuccess()) {
-                            // Impossible case, we just set forceOverride to be true, we should
-                            // never fail in incompatible changes. And all other cases should failed
-                            // during the first call.
-                            // TODO(b/261897334) save SDK errors/crashes and send to server for
-                            //  logging.
+                        InternalSetSchemaResponse internalSetSchemaResponse1 =
+                                setSchemaResult.getResultValue();
+                        if (internalSetSchemaResponse1 == null) {
                             safeExecute(
                                     callbackExecutor,
                                     callback,
-                                    () -> callback.accept(
-                                            AppSearchResult.newFailedResult(
-                                                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                                                    internalSetSchemaResponse2.getErrorMessage())));
+                                    () ->
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(
+                                                            RESULT_INTERNAL_ERROR,
+                                                            "Received null"
+                                                                    + " InternalSetSchemaResponse"
+                                                                    + " during setSchema call")));
                             return;
                         }
-                        internalSetSchemaResponse = internalSetSchemaResponse2;
+                        long firstSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
+
+                        // 3. If forceOverride is false, check that all incompatible types will be
+                        // migrated.
+                        // If some aren't we must throw an error, rather than proceeding and
+                        // deleting those
+                        // types.
+                        SchemaMigrationUtil.checkDeletedAndIncompatibleAfterMigration(
+                                internalSetSchemaResponse1, activeMigrators.keySet());
+
+                        try (AppSearchMigrationHelper migrationHelper =
+                                new AppSearchMigrationHelper(
+                                        mService,
+                                        mUserHandle,
+                                        mCallerAttributionSource,
+                                        mDatabaseName,
+                                        request.getSchemas(),
+                                        mCacheDirectory)) {
+
+                            // 4. Trigger migration for all migrators.
+                            long queryAndTransformLatencyStartMillis =
+                                    SystemClock.elapsedRealtime();
+                            for (Map.Entry<String, Migrator> entry : activeMigrators.entrySet()) {
+                                migrationHelper.queryAndTransform(
+                                        /* schemaType= */ entry.getKey(),
+                                        /* migrator= */ entry.getValue(),
+                                        currentVersion,
+                                        finalVersion,
+                                        statsBuilder);
+                            }
+                            long queryAndTransformLatencyEndTimeMillis =
+                                    SystemClock.elapsedRealtime();
+
+                            // 5. SetSchema a second time with forceOverride=true if the first
+                            // attempted
+                            // failed.
+                            long secondSetSchemaLatencyStartMillis = SystemClock.elapsedRealtime();
+                            InternalSetSchemaResponse internalSetSchemaResponse;
+                            if (internalSetSchemaResponse1.isSuccess()) {
+                                internalSetSchemaResponse = internalSetSchemaResponse1;
+                            } else {
+                                CountDownLatch setSchema2Latch = new CountDownLatch(1);
+                                AtomicReference<AppSearchResult<InternalSetSchemaResponse>>
+                                        setSchema2ResultRef = new AtomicReference<>();
+                                // only trigger second setSchema() call if the first one is fail.
+                                SetSchemaAidlRequest setSchemaAidlRequest1 =
+                                        new SetSchemaAidlRequest(
+                                                mCallerAttributionSource,
+                                                mDatabaseName,
+                                                schemas,
+                                                visibilityConfigs,
+                                                /* forceOverride= */ true,
+                                                request.getVersion(),
+                                                mUserHandle,
+                                                /* binderCallStartTimeMillis= */ SystemClock
+                                                        .elapsedRealtime(),
+                                                SchemaMigrationStats.SECOND_CALL_APPLY_NEW_SCHEMA);
+                                mService.setSchema(
+                                        setSchemaAidlRequest1,
+                                        new IAppSearchResultCallback.Stub() {
+                                            @Override
+                                            @SuppressWarnings({"rawtypes", "unchecked"})
+                                            public void onResult(
+                                                    AppSearchResultParcel resultParcel) {
+                                                setSchema2ResultRef.set(resultParcel.getResult());
+                                                setSchema2Latch.countDown();
+                                            }
+                                        });
+                                setSchema2Latch.await();
+                                AppSearchResult<InternalSetSchemaResponse> setSchema2Result =
+                                        setSchema2ResultRef.get();
+                                if (!setSchema2Result.isSuccess()) {
+                                    // we failed to set the schema in second time with forceOverride
+                                    // = true, which is an impossible case. Since we only swallow
+                                    // the incompatible error in the first setSchema call, all other
+                                    // errors will be thrown at the first time.
+                                    // TODO(b/261897334) save SDK errors/crashes and send to server
+                                    //  for logging.
+                                    safeExecute(
+                                            callbackExecutor,
+                                            callback,
+                                            () ->
+                                                    callback.accept(
+                                                            AppSearchResult.newFailedResult(
+                                                                    setSchema2Result)));
+                                    return;
+                                }
+                                InternalSetSchemaResponse internalSetSchemaResponse2 =
+                                        setSchema2Result.getResultValue();
+                                if (internalSetSchemaResponse2 == null) {
+                                    safeExecute(
+                                            callbackExecutor,
+                                            callback,
+                                            () ->
+                                                    callback.accept(
+                                                            AppSearchResult.newFailedResult(
+                                                                    RESULT_INTERNAL_ERROR,
+                                                                    "Received null response"
+                                                                            + " during setSchema"
+                                                                            + " call")));
+                                    return;
+                                }
+                                if (!internalSetSchemaResponse2.isSuccess()) {
+                                    // Impossible case, we just set forceOverride to be true, we
+                                    // should never fail in incompatible changes. And all other
+                                    // cases should failed during the first call.
+                                    // TODO(b/261897334) save SDK errors/crashes and send to server
+                                    //  for logging.
+                                    safeExecute(
+                                            callbackExecutor,
+                                            callback,
+                                            () ->
+                                                    callback.accept(
+                                                            AppSearchResult.newFailedResult(
+                                                                    RESULT_INTERNAL_ERROR,
+                                                                    internalSetSchemaResponse2
+                                                                            .getErrorMessage())));
+                                    return;
+                                }
+                                internalSetSchemaResponse = internalSetSchemaResponse2;
+                            }
+                            long secondSetSchemaLatencyEndTimeMillis =
+                                    SystemClock.elapsedRealtime();
+
+                            statsBuilder
+                                    .setExecutorAcquisitionLatencyMillis(
+                                            (int)
+                                                    (waitExecutorEndLatencyMillis
+                                                            - waitExecutorStartLatencyMillis))
+                                    .setGetSchemaLatencyMillis(
+                                            (int)
+                                                    (getSchemaLatencyEndTimeMillis
+                                                            - getSchemaLatencyStartTimeMillis))
+                                    .setFirstSetSchemaLatencyMillis(
+                                            (int)
+                                                    (firstSetSchemaLatencyEndTimeMillis
+                                                            - firstSetSchemaLatencyStartMillis))
+                                    .setIsFirstSetSchemaSuccess(
+                                            internalSetSchemaResponse1.isSuccess())
+                                    .setQueryAndTransformLatencyMillis(
+                                            (int)
+                                                    (queryAndTransformLatencyEndTimeMillis
+                                                            - queryAndTransformLatencyStartMillis))
+                                    .setSecondSetSchemaLatencyMillis(
+                                            (int)
+                                                    (secondSetSchemaLatencyEndTimeMillis
+                                                            - secondSetSchemaLatencyStartMillis));
+                            SetSchemaResponse.Builder responseBuilder =
+                                    new SetSchemaResponse.Builder(
+                                                    internalSetSchemaResponse
+                                                            .getSetSchemaResponse())
+                                            .addMigratedTypes(activeMigrators.keySet());
+
+                            // 6. Put all the migrated documents into the index, now that the new
+                            // schema is
+                            // set.
+                            AppSearchResult<SetSchemaResponse> putResult =
+                                    migrationHelper.putMigratedDocuments(
+                                            responseBuilder,
+                                            statsBuilder,
+                                            totalLatencyStartTimeMillis);
+                            safeExecute(
+                                    callbackExecutor, callback, () -> callback.accept(putResult));
+                        }
+                    } catch (RemoteException
+                            | AppSearchException
+                            | InterruptedException
+                            | IOException
+                            | ExecutionException
+                            | RuntimeException e) {
+                        // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
+                        safeExecute(
+                                callbackExecutor,
+                                callback,
+                                () -> callback.accept(AppSearchResult.throwableToFailedResult(e)));
                     }
-                    long secondSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
-
-                    statsBuilder
-                            .setExecutorAcquisitionLatencyMillis(
-                                    (int) (waitExecutorEndLatencyMillis
-                                            - waitExecutorStartLatencyMillis))
-                            .setGetSchemaLatencyMillis(
-                                    (int)(getSchemaLatencyEndTimeMillis
-                                            - getSchemaLatencyStartTimeMillis))
-                            .setFirstSetSchemaLatencyMillis(
-                                    (int)(firstSetSchemaLatencyEndTimeMillis
-                                            - firstSetSchemaLatencyStartMillis))
-                            .setIsFirstSetSchemaSuccess(internalSetSchemaResponse1.isSuccess())
-                            .setQueryAndTransformLatencyMillis(
-                                    (int)(queryAndTransformLatencyEndTimeMillis -
-                                            queryAndTransformLatencyStartTimeMillis))
-                            .setSecondSetSchemaLatencyMillis(
-                                    (int)(secondSetSchemaLatencyEndTimeMillis
-                                            - secondSetSchemaLatencyStartMillis));
-                    SetSchemaResponse.Builder responseBuilder = new SetSchemaResponse.Builder(
-                            internalSetSchemaResponse.getSetSchemaResponse())
-                            .addMigratedTypes(activeMigrators.keySet());
-
-                    // 6. Put all the migrated documents into the index, now that the new schema is
-                    // set.
-                    AppSearchResult<SetSchemaResponse> putResult =
-                            migrationHelper.putMigratedDocuments(
-                                    responseBuilder, statsBuilder, totalLatencyStartTimeMillis);
-                    safeExecute(callbackExecutor, callback, () -> callback.accept(putResult));
-                }
-            } catch (RemoteException
-                     | AppSearchException
-                     | InterruptedException
-                     | IOException
-                     | ExecutionException
-                     | RuntimeException e) {
-                // TODO(b/261897334) save SDK errors/crashes and send to server for logging.
-                safeExecute(
-                        callbackExecutor,
-                        callback,
-                        () -> callback.accept(AppSearchResult.throwableToFailedResult(e)));
-            }
-        });
+                });
     }
 
     @NonNull
diff --git a/framework/java/android/app/appsearch/BatchResultCallback.java b/framework/java/android/app/appsearch/BatchResultCallback.java
index 28f8a7a..f49a3ed 100644
--- a/framework/java/android/app/appsearch/BatchResultCallback.java
+++ b/framework/java/android/app/appsearch/BatchResultCallback.java
@@ -22,8 +22,8 @@
 /**
  * The callback interface to return {@link AppSearchBatchResult}.
  *
- * @param <KeyType> The type of the keys for {@link AppSearchBatchResult#getSuccesses} and
- * {@link AppSearchBatchResult#getFailures}.
+ * @param <KeyType> The type of the keys for {@link AppSearchBatchResult#getSuccesses} and {@link
+ *     AppSearchBatchResult#getFailures}.
  * @param <ValueType> The type of result objects associated with the keys.
  */
 public interface BatchResultCallback<KeyType, ValueType> {
@@ -47,8 +47,8 @@
      * <p>The error is not expected to be recoverable and there is no specific recommended action
      * other than displaying a permanent message to the user.
      *
-     * <p>Normal errors that are caused by invalid inputs or recoverable/retriable situations
-     * are reported associated with the input that caused them via the {@link #onResult} method.
+     * <p>Normal errors that are caused by invalid inputs or recoverable/retriable situations are
+     * reported associated with the input that caused them via the {@link #onResult} method.
      *
      * @param throwable an exception describing the system error
      */
diff --git a/framework/java/android/app/appsearch/EnterpriseGlobalSearchSession.java b/framework/java/android/app/appsearch/EnterpriseGlobalSearchSession.java
index 5593c2a..e01f4d3 100644
--- a/framework/java/android/app/appsearch/EnterpriseGlobalSearchSession.java
+++ b/framework/java/android/app/appsearch/EnterpriseGlobalSearchSession.java
@@ -21,9 +21,10 @@
 import android.annotation.NonNull;
 import android.app.appsearch.aidl.AppSearchAttributionSource;
 import android.app.appsearch.aidl.IAppSearchManager;
-import android.app.appsearch.flags.Flags;
 import android.os.UserHandle;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -52,18 +53,22 @@
             @NonNull Consumer<AppSearchResult<EnterpriseGlobalSearchSession>> callback) {
         EnterpriseGlobalSearchSession enterpriseGlobalSearchSession =
                 new EnterpriseGlobalSearchSession(service, userHandle, attributionSource);
-        enterpriseGlobalSearchSession.initialize(executor, result -> {
-            if (result.isSuccess()) {
-                callback.accept(AppSearchResult.newSuccessfulResult(enterpriseGlobalSearchSession));
-            } else {
-                callback.accept(AppSearchResult.newFailedResult(result));
-            }
-        });
+        enterpriseGlobalSearchSession.initialize(
+                executor,
+                result -> {
+                    if (result.isSuccess()) {
+                        callback.accept(
+                                AppSearchResult.newSuccessfulResult(enterpriseGlobalSearchSession));
+                    } else {
+                        callback.accept(AppSearchResult.newFailedResult(result));
+                    }
+                });
     }
 
-    private EnterpriseGlobalSearchSession(@NonNull IAppSearchManager service,
+    private EnterpriseGlobalSearchSession(
+            @NonNull IAppSearchManager service,
             @NonNull UserHandle userHandle,
             @NonNull AppSearchAttributionSource callerAttributionSource) {
-        super(service, userHandle, callerAttributionSource, /*isForEnterprise=*/ true);
+        super(service, userHandle, callerAttributionSource, /* isForEnterprise= */ true);
     }
 }
diff --git a/framework/java/android/app/appsearch/FrameworkAppSearchEnvironment.java b/framework/java/android/app/appsearch/FrameworkAppSearchEnvironment.java
index b5f7bcd..adecb94 100644
--- a/framework/java/android/app/appsearch/FrameworkAppSearchEnvironment.java
+++ b/framework/java/android/app/appsearch/FrameworkAppSearchEnvironment.java
@@ -18,79 +18,86 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.appsearch.AppSearchEnvironment;
 import android.content.Context;
 import android.os.Environment;
 import android.os.UserHandle;
 
 import java.io.File;
+import java.util.Objects;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Executors;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import java.util.Objects;
 
 /**
  * Contains utility methods for Framework implementation of AppSearch.
+ *
  * @hide
  */
 public class FrameworkAppSearchEnvironment implements AppSearchEnvironment {
 
-  /**
-   * Returns AppSearch directory in the credential encrypted system directory for the given user.
-   *
-   * <p>This folder should only be accessed after unlock.
-   */
-  @Override
-  public File getAppSearchDir(@NonNull Context unused, @NonNull UserHandle userHandle) {
-    // Duplicates the implementation of Environment#getDataSystemCeDirectory
-    // TODO(b/191059409): Unhide Environment#getDataSystemCeDirectory and switch to it.
-    Objects.requireNonNull(userHandle);
-    File systemCeDir = new File(Environment.getDataDirectory(), "system_ce");
-    File systemCeUserDir = new File(systemCeDir, String.valueOf(userHandle.getIdentifier()));
-    return new File(systemCeUserDir, "appsearch");
-  }
+    /**
+     * Returns AppSearch directory in the credential encrypted system directory for the given user.
+     *
+     * <p>This folder should only be accessed after unlock.
+     */
+    @Override
+    public File getAppSearchDir(@NonNull Context unused, @NonNull UserHandle userHandle) {
+        // Duplicates the implementation of Environment#getDataSystemCeDirectory
+        // TODO(b/191059409): Unhide Environment#getDataSystemCeDirectory and switch to it.
+        Objects.requireNonNull(userHandle);
+        File systemCeDir = new File(Environment.getDataDirectory(), "system_ce");
+        File systemCeUserDir = new File(systemCeDir, String.valueOf(userHandle.getIdentifier()));
+        return new File(systemCeUserDir, "appsearch");
+    }
 
-  /** Creates context for the user based on the userHandle. */
-  @Override
-  public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle) {
-    Objects.requireNonNull(context);
-    Objects.requireNonNull(userHandle);
-    return context.createContextAsUser(userHandle, /*flags=*/ 0);
-  }
+    /** Creates context for the user based on the userHandle. */
+    @Override
+    public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(userHandle);
+        return context.createContextAsUser(userHandle, /* flags= */ 0);
+    }
 
-  /** Creates and returns a ThreadPoolExecutor for given parameters. */
-  @Override
-  public ExecutorService createExecutorService(
-      int corePoolSize,
-      int maxConcurrency,
-      long keepAliveTime,
-      TimeUnit unit,
-      BlockingQueue<Runnable> workQueue,
-      int priority) {
-    return new ThreadPoolExecutor(
-        corePoolSize,
-        maxConcurrency,
-        keepAliveTime,
-        unit,
-        workQueue);
-  }
+    /** Creates and returns a ThreadPoolExecutor for given parameters. */
+    @Override
+    public ExecutorService createExecutorService(
+            int corePoolSize,
+            int maxConcurrency,
+            long keepAliveTime,
+            TimeUnit unit,
+            BlockingQueue<Runnable> workQueue,
+            int priority) {
+        return new ThreadPoolExecutor(corePoolSize, maxConcurrency, keepAliveTime, unit, workQueue);
+    }
 
-  /** Createsand returns an ExecutorService with a single thread. */
-  @Override
-  public ExecutorService createSingleThreadExecutor() {
-    return Executors.newSingleThreadExecutor();
-  }
+    /** Createsand returns an ExecutorService with a single thread. */
+    @Override
+    public ExecutorService createSingleThreadExecutor() {
+        return Executors.newSingleThreadExecutor();
+    }
 
-  /**
-   * Returns a cache directory for creating temporary files like in case of migrating documents.
-   */
-  @Override
-  @Nullable
-  public File getCacheDir(@NonNull Context context) {
-    // Framework/Android does not have app-specific cache directory.
-    return null;
-  }
+    /** Creates and returns an Executor with cached thread pools. */
+    @NonNull
+    @Override
+    public ExecutorService createCachedThreadPoolExecutor() {
+        return Executors.newCachedThreadPool();
+    }
+
+    /**
+     * Returns a cache directory for creating temporary files like in case of migrating documents.
+     */
+    @Override
+    @Nullable
+    public File getCacheDir(@NonNull Context context) {
+        // Framework/Android does not have app-specific cache directory.
+        return null;
+    }
+
+    /** Returns if we can log INFO level logs. */
+    @Override
+    public boolean isInfoLoggingEnabled() {
+        return true;
+    }
 }
-
diff --git a/framework/java/android/app/appsearch/GlobalSearchSession.java b/framework/java/android/app/appsearch/GlobalSearchSession.java
index fdbb2d1..defb64a 100644
--- a/framework/java/android/app/appsearch/GlobalSearchSession.java
+++ b/framework/java/android/app/appsearch/GlobalSearchSession.java
@@ -27,6 +27,7 @@
 import android.app.appsearch.aidl.IAppSearchResultCallback;
 import android.app.appsearch.aidl.PersistToDiskAidlRequest;
 import android.app.appsearch.aidl.RegisterObserverCallbackAidlRequest;
+import android.app.appsearch.aidl.ReportUsageAidlRequest;
 import android.app.appsearch.aidl.UnregisterObserverCallbackAidlRequest;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.app.appsearch.observer.DocumentChangeInfo;
@@ -70,8 +71,8 @@
     private boolean mIsClosed = false;
 
     /**
-     * Creates a search session for the client, defined by the {@code userHandle} and
-     * {@code packageName}.
+     * Creates a search session for the client, defined by the {@code userHandle} and {@code
+     * packageName}.
      */
     static void createGlobalSearchSession(
             @NonNull IAppSearchManager service,
@@ -79,44 +80,46 @@
             @NonNull AppSearchAttributionSource attributionSource,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<AppSearchResult<GlobalSearchSession>> callback) {
-        GlobalSearchSession globalSearchSession = new GlobalSearchSession(service, userHandle,
-                attributionSource);
-        globalSearchSession.initialize(executor, result -> {
-            if (result.isSuccess()) {
-                callback.accept(AppSearchResult.newSuccessfulResult(globalSearchSession));
-            } else {
-                callback.accept(AppSearchResult.newFailedResult(result));
-            }
-        });
+        GlobalSearchSession globalSearchSession =
+                new GlobalSearchSession(service, userHandle, attributionSource);
+        globalSearchSession.initialize(
+                executor,
+                result -> {
+                    if (result.isSuccess()) {
+                        callback.accept(AppSearchResult.newSuccessfulResult(globalSearchSession));
+                    } else {
+                        callback.accept(AppSearchResult.newFailedResult(result));
+                    }
+                });
     }
 
-    private GlobalSearchSession(@NonNull IAppSearchManager service, @NonNull UserHandle userHandle,
+    private GlobalSearchSession(
+            @NonNull IAppSearchManager service,
+            @NonNull UserHandle userHandle,
             @NonNull AppSearchAttributionSource callerAttributionSource) {
-        super(service, userHandle, callerAttributionSource, /*isForEnterprise=*/ false);
+        super(service, userHandle, callerAttributionSource, /* isForEnterprise= */ false);
     }
 
     /**
      * Retrieves {@link GenericDocument} documents, belonging to the specified package name and
-     * database name and identified by the namespace and ids in the request, from the
-     * {@link GlobalSearchSession} database.
+     * database name and identified by the namespace and ids in the request, from the {@link
+     * GlobalSearchSession} database.
      *
      * <p>If the package or database doesn't exist or if the calling package doesn't have access,
      * the gets will be handled as failures in an {@link AppSearchBatchResult} object in the
      * callback.
      *
-     * @param packageName  the name of the package to get from
+     * @param packageName the name of the package to get from
      * @param databaseName the name of the database to get from
-     * @param request      a request containing a namespace and IDs to get documents for.
-     * @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 IDs. The
-     *                     values are the returned {@link GenericDocument}s on success, or a failed
-     *                     {@link AppSearchResult} otherwise. IDs that are not found will return a
-     *                     failed {@link AppSearchResult} with a result code of
-     *                     {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
-     *                     occurs in the AppSearch service,
-     *                     {@link BatchResultCallback#onSystemError} will be invoked with a
-     *                     {@link Throwable}.
+     * @param request a request containing a namespace and IDs to get documents for.
+     * @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 IDs. The values are the
+     *     returned {@link GenericDocument}s on success, or a failed {@link AppSearchResult}
+     *     otherwise. IDs that are not found will return a failed {@link AppSearchResult} with a
+     *     result code of {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
+     *     occurs in the AppSearch service, {@link BatchResultCallback#onSystemError} will be
+     *     invoked with a {@link Throwable}.
      */
     @Override
     public void getByDocumentId(
@@ -144,8 +147,8 @@
      * SearchResults#getNextPage}.
      *
      * @param queryExpression query string to search.
-     * @param searchSpec      spec for setting document filters, adding projection, setting term
-     *                        match type, etc.
+     * @param searchSpec spec for setting document filters, adding projection, setting term match
+     *     type, etc.
      * @return a {@link SearchResults} object for retrieved matched documents.
      */
     @NonNull
@@ -163,11 +166,11 @@
      * <p>If the requested package/database combination does not exist or the caller has not been
      * granted access to it, then an empty GetSchemaResponse will be returned.
      *
-     * @param packageName  the package that owns the requested {@link AppSearchSchema} instances.
+     * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
      * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
      * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
-     *         access to or an empty GetSchemaResponse if the request package and database does not
-     *         exist, has not set a schema or contains no schemas that are accessible to the caller.
+     *     access to or an empty GetSchemaResponse if the request package and database does not
+     *     exist, has not set a schema or contains no schemas that are accessible to the caller.
      */
     @Override
     public void getSchema(
@@ -185,18 +188,17 @@
      * <p>See {@link AppSearchSession#reportUsage} for a general description of document usage, as
      * well as an API that can be used by the app itself.
      *
-     * <p>Usage reported via this method is accounted separately from usage reported via
-     * {@link AppSearchSession#reportUsage} and may be accessed using the constants
-     * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and
-     * {@link SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
+     * <p>Usage reported via this method is accounted separately from usage reported via {@link
+     * AppSearchSession#reportUsage} and may be accessed using the constants {@link
+     * SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_COUNT} and {@link
+     * SearchSpec#RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP}.
      *
-     * @param request  The usage reporting request.
+     * @param request The usage reporting request.
      * @param executor Executor on which to invoke the callback.
      * @param callback Callback to receive errors. If the operation succeeds, the callback will be
-     *                 invoked with an {@link AppSearchResult} whose value is {@code null}. The
-     *                 callback will be invoked with an {@link AppSearchResult} of
-     *                 {@link AppSearchResult#RESULT_SECURITY_ERROR} if this API is invoked by an
-     *                 app which is not part of the system.
+     *     invoked with an {@link AppSearchResult} whose value is {@code null}. The callback will be
+     *     invoked with an {@link AppSearchResult} of {@link AppSearchResult#RESULT_SECURITY_ERROR}
+     *     if this API is invoked by an app which is not part of the system.
      */
     public void reportSystemUsage(
             @NonNull ReportSystemUsageRequest request,
@@ -208,17 +210,20 @@
         Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
         try {
             mService.reportUsage(
-                    mCallerAttributionSource,
-                    request.getPackageName(),
-                    request.getDatabaseName(),
-                    request.getNamespace(),
-                    request.getDocumentId(),
-                    request.getUsageTimestampMillis(),
-                    /*systemUsage=*/ true,
-                    mUserHandle,
-                    /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+                    new ReportUsageAidlRequest(
+                            mCallerAttributionSource,
+                            request.getPackageName(),
+                            request.getDatabaseName(),
+                            new ReportUsageRequest(
+                                    request.getNamespace(),
+                                    request.getDocumentId(),
+                                    request.getUsageTimestampMillis()),
+                            /* systemUsage= */ true,
+                            mUserHandle,
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
                             safeExecute(
                                     executor,
@@ -233,9 +238,9 @@
     }
 
     /**
-     * Adds an {@link ObserverCallback} to monitor changes within the databases owned by
-     * {@code targetPackageName} if they match the given
-     * {@link android.app.appsearch.observer.ObserverSpec}.
+     * Adds an {@link ObserverCallback} to monitor changes within the databases owned by {@code
+     * targetPackageName} if they match the given {@link
+     * android.app.appsearch.observer.ObserverSpec}.
      *
      * <p>The observer callback is only triggered for data that changes after it is registered. No
      * notification about existing data is sent as a result of registering an observer. To find out
@@ -250,16 +255,18 @@
      * later if {@code targetPackageName} is installed and starts indexing data.
      *
      * @param targetPackageName Package whose changes to monitor
-     * @param spec              Specification of what types of changes to listen for
-     * @param executor          Executor on which to call the {@code observer} callback methods.
-     * @param observer          Callback to trigger when a schema or document changes
+     * @param spec Specification of what types of changes to listen for
+     * @param executor Executor on which to call the {@code observer} callback methods.
+     * @param observer Callback to trigger when a schema or document changes
      * @throws AppSearchException If an unexpected error occurs when trying to register an observer.
      */
+    @SuppressWarnings("unchecked")
     public void registerObserverCallback(
             @NonNull String targetPackageName,
             @NonNull ObserverSpec spec,
             @NonNull Executor executor,
-            @NonNull ObserverCallback observer) throws AppSearchException {
+            @NonNull ObserverCallback observer)
+            throws AppSearchException {
         Objects.requireNonNull(targetPackageName);
         Objects.requireNonNull(spec);
         Objects.requireNonNull(executor);
@@ -275,62 +282,76 @@
             }
             if (stub == null) {
                 // No stub is associated with this package and observer, so we must create one.
-                stub = new IAppSearchObserverProxy.Stub() {
-                    @Override
-                    public void onSchemaChanged(
-                            @NonNull String packageName,
-                            @NonNull String databaseName,
-                            @NonNull List<String> changedSchemaNames) {
-                        safeExecute(executor, this::suppressingErrorCallback, () -> {
-                            SchemaChangeInfo changeInfo = new SchemaChangeInfo(
-                                    packageName, databaseName, new ArraySet<>(changedSchemaNames));
-                            observer.onSchemaChanged(changeInfo);
-                        });
-                    }
+                stub =
+                        new IAppSearchObserverProxy.Stub() {
+                            @Override
+                            public void onSchemaChanged(
+                                    @NonNull String packageName,
+                                    @NonNull String databaseName,
+                                    @NonNull List<String> changedSchemaNames) {
+                                safeExecute(
+                                        executor,
+                                        this::suppressingErrorCallback,
+                                        () -> {
+                                            SchemaChangeInfo changeInfo =
+                                                    new SchemaChangeInfo(
+                                                            packageName,
+                                                            databaseName,
+                                                            new ArraySet<>(changedSchemaNames));
+                                            observer.onSchemaChanged(changeInfo);
+                                        });
+                            }
 
-                    @Override
-                    public void onDocumentChanged(
-                            @NonNull String packageName,
-                            @NonNull String databaseName,
-                            @NonNull String namespace,
-                            @NonNull String schemaName,
-                            @NonNull List<String> changedDocumentIds) {
-                        safeExecute(executor, this::suppressingErrorCallback, () -> {
-                            DocumentChangeInfo changeInfo = new DocumentChangeInfo(
-                                    packageName,
-                                    databaseName,
-                                    namespace,
-                                    schemaName,
-                                    new ArraySet<>(changedDocumentIds));
-                            observer.onDocumentChanged(changeInfo);
-                        });
-                    }
+                            @Override
+                            public void onDocumentChanged(
+                                    @NonNull String packageName,
+                                    @NonNull String databaseName,
+                                    @NonNull String namespace,
+                                    @NonNull String schemaName,
+                                    @NonNull List<String> changedDocumentIds) {
+                                safeExecute(
+                                        executor,
+                                        this::suppressingErrorCallback,
+                                        () -> {
+                                            DocumentChangeInfo changeInfo =
+                                                    new DocumentChangeInfo(
+                                                            packageName,
+                                                            databaseName,
+                                                            namespace,
+                                                            schemaName,
+                                                            new ArraySet<>(changedDocumentIds));
+                                            observer.onDocumentChanged(changeInfo);
+                                        });
+                            }
 
-                    /**
-                     * Error-handling callback that simply drops errors.
-                     *
-                     * <p>If we fail to deliver change notifications, there isn't much we can do.
-                     * The API doesn't allow the user to provide a callback to invoke on failure of
-                     * change notification delivery. {@link SearchSessionUtil#safeExecute} already
-                     * includes a log message. So we just do nothing.
-                     */
-                    private void suppressingErrorCallback(@NonNull AppSearchResult<?> unused) {
-                    }
-                };
+                            /**
+                             * Error-handling callback that simply drops errors.
+                             *
+                             * <p>If we fail to deliver change notifications, there isn't much we
+                             * can do. The API doesn't allow the user to provide a callback to
+                             * invoke on failure of change notification delivery. {@link
+                             * SearchSessionUtil#safeExecute} already includes a log message. So we
+                             * just do nothing.
+                             */
+                            private void suppressingErrorCallback(
+                                    @NonNull AppSearchResult<?> unused) {}
+                        };
             }
 
             // Regardless of whether this stub was fresh or not, we have to register it again
             // because the user might be supplying a different spec.
             AppSearchResultParcel<Void> resultParcel;
             try {
-                resultParcel = mService.registerObserverCallback(
-                        new RegisterObserverCallbackAidlRequest(
-                                mCallerAttributionSource,
-                                targetPackageName,
-                                spec,
-                                mUserHandle,
-                                /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
-                        stub);
+                resultParcel =
+                        mService.registerObserverCallback(
+                                new RegisterObserverCallbackAidlRequest(
+                                        mCallerAttributionSource,
+                                        targetPackageName,
+                                        spec,
+                                        mUserHandle,
+                                        /* binderCallStartTimeMillis= */ SystemClock
+                                                .elapsedRealtime()),
+                                stub);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -355,23 +376,23 @@
     /**
      * Removes previously registered {@link ObserverCallback} instances from the system.
      *
-     * <p>All instances of {@link ObserverCallback} which are registered to observe
-     * {@code targetPackageName} and compare equal to the provided callback using the provided
-     * argument's {@code ObserverCallback#equals} will be removed.
+     * <p>All instances of {@link ObserverCallback} which are registered to observe {@code
+     * targetPackageName} and compare equal to the provided callback using the provided argument's
+     * {@code ObserverCallback#equals} will be removed.
      *
      * <p>If no matching observers have been registered, this method has no effect. If multiple
      * matching observers have been registered, all will be removed.
      *
      * @param targetPackageName Package which the observers to be removed are listening to.
-     * @param observer          Callback to unregister.
+     * @param observer Callback to unregister.
      * @throws AppSearchException if an error occurs trying to remove the observer, such as a
-     *                            failure to communicate with the system service. Note that no error
-     *                            will be thrown if the provided observer doesn't match any
-     *                            registered observer.
+     *     failure to communicate with the system service. Note that no error will be thrown if the
+     *     provided observer doesn't match any registered observer.
      */
+    @SuppressWarnings("unchecked")
     public void unregisterObserverCallback(
-            @NonNull String targetPackageName,
-            @NonNull ObserverCallback observer) throws AppSearchException {
+            @NonNull String targetPackageName, @NonNull ObserverCallback observer)
+            throws AppSearchException {
         Objects.requireNonNull(targetPackageName);
         Objects.requireNonNull(observer);
         Preconditions.checkState(!mIsClosed, "GlobalSearchSession has already been closed");
@@ -381,22 +402,24 @@
             Map<ObserverCallback, IAppSearchObserverProxy> observersForPackage =
                     mObserverCallbacksLocked.get(targetPackageName);
             if (observersForPackage == null) {
-                return;  // No observers registered for this package. Nothing to do.
+                return; // No observers registered for this package. Nothing to do.
             }
             stub = observersForPackage.get(observer);
             if (stub == null) {
-                return;  // No such observer registered. Nothing to do.
+                return; // No such observer registered. Nothing to do.
             }
 
             AppSearchResultParcel<Void> resultParcel;
             try {
-                resultParcel = mService.unregisterObserverCallback(
-                        new UnregisterObserverCallbackAidlRequest(
-                                mCallerAttributionSource,
-                                targetPackageName,
-                                mUserHandle,
-                                /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
-                        stub);
+                resultParcel =
+                        mService.unregisterObserverCallback(
+                                new UnregisterObserverCallbackAidlRequest(
+                                        mCallerAttributionSource,
+                                        targetPackageName,
+                                        mUserHandle,
+                                        /* binderCallStartTimeMillis= */ SystemClock
+                                                .elapsedRealtime()),
+                                stub);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -426,7 +449,7 @@
                         new PersistToDiskAidlRequest(
                                 mCallerAttributionSource,
                                 mUserHandle,
-                                /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()));
+                                /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()));
                 mIsClosed = true;
             } catch (RemoteException e) {
                 Log.e(TAG, "Unable to close the GlobalSearchSession", e);
diff --git a/framework/java/android/app/appsearch/ParcelableUtil.java b/framework/java/android/app/appsearch/ParcelableUtil.java
index dc7183c..3e0fe78 100644
--- a/framework/java/android/app/appsearch/ParcelableUtil.java
+++ b/framework/java/android/app/appsearch/ParcelableUtil.java
@@ -17,6 +17,7 @@
 package android.app.appsearch;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.os.Build;
 import android.os.Parcel;
@@ -29,7 +30,8 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 
-/** Wrapper class to provide implementation for readBlob/writeBlob for all API levels.
+/**
+ * Wrapper class to provide implementation for readBlob/writeBlob for all API levels.
  *
  * @hide
  */
@@ -41,7 +43,6 @@
     // under the transaction buffer limit.
     private static final int DOCUMENT_SIZE_LIMIT_IN_BYTES = 64 * 1024;
 
-
     // TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
     @SuppressLint("ObsoleteSdkInt")
     public static void writeBlob(@NonNull Parcel parcel, @NonNull byte[] bytes) {
@@ -60,8 +61,7 @@
             if (bytes.length <= DOCUMENT_SIZE_LIMIT_IN_BYTES) {
                 parcel.writeByteArray(bytes);
             } else {
-                ParcelFileDescriptor parcelFileDescriptor =
-                        writeDataToTempFileAndUnlinkFile(bytes);
+                ParcelFileDescriptor parcelFileDescriptor = writeDataToTempFileAndUnlinkFile(bytes);
                 parcel.writeFileDescriptor(parcelFileDescriptor.getFileDescriptor());
             }
         } catch (IOException e) {
@@ -70,7 +70,8 @@
         }
     }
 
-    @NonNull
+    /** Calls parcel#readBlob on T+ and uses byte array or PFD on lower API levels. */
+    @Nullable
     // TODO(b/232805516): Update SDK_INT in Android.bp to safeguard from unexpected compiler issues.
     @SuppressLint("ObsoleteSdkInt")
     public static byte[] readBlob(Parcel parcel) {
@@ -83,6 +84,7 @@
         }
     }
 
+    @Nullable
     private static byte[] readFromParcelForSAndBelow(Parcel parcel) {
         try {
             int length = parcel.readInt();
@@ -102,16 +104,15 @@
     }
 
     /**
-     * Reads data bytes from file using provided FileDescriptor. It also closes the PFD so that
-     * will delete the underlying file if it's the only reference left.
+     * Reads data bytes from file using provided FileDescriptor. It also closes the PFD so that will
+     * delete the underlying file if it's the only reference left.
      *
      * @param pfd ParcelFileDescriptor for the file to read.
      * @param length Number of bytes to read from the file.
      */
-    private static byte[] getDataFromFd(ParcelFileDescriptor pfd,
-            int length) throws IOException {
-        try(DataInputStream in =
-                new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd))){
+    private static byte[] getDataFromFd(ParcelFileDescriptor pfd, int length) throws IOException {
+        try (DataInputStream in =
+                new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(pfd))) {
             byte[] data = new byte[length];
             in.read(data);
             return data;
@@ -129,14 +130,14 @@
         // TODO(b/232959004):  Update directory to app-specific cache dir instead of null.
         File unlinkedFile =
                 File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX, /* directory= */ null);
-        try(DataOutputStream out = new DataOutputStream(new FileOutputStream(unlinkedFile))) {
+        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(unlinkedFile))) {
             out.write(data);
             out.flush();
         }
         ParcelFileDescriptor parcelFileDescriptor =
-                ParcelFileDescriptor.open(unlinkedFile,
-                        ParcelFileDescriptor.MODE_CREATE
-                                | ParcelFileDescriptor.MODE_READ_WRITE);
+                ParcelFileDescriptor.open(
+                        unlinkedFile,
+                        ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE);
         unlinkedFile.delete();
         return parcelFileDescriptor;
     }
diff --git a/framework/java/android/app/appsearch/ReadOnlyGlobalSearchSession.java b/framework/java/android/app/appsearch/ReadOnlyGlobalSearchSession.java
index ab1c478..a50fbaf 100644
--- a/framework/java/android/app/appsearch/ReadOnlyGlobalSearchSession.java
+++ b/framework/java/android/app/appsearch/ReadOnlyGlobalSearchSession.java
@@ -22,17 +22,18 @@
 import android.annotation.NonNull;
 import android.app.appsearch.aidl.AppSearchAttributionSource;
 import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.GetDocumentsAidlRequest;
 import android.app.appsearch.aidl.GetSchemaAidlRequest;
 import android.app.appsearch.aidl.IAppSearchManager;
 import android.app.appsearch.aidl.IAppSearchResultCallback;
 import android.app.appsearch.aidl.InitializeAidlRequest;
+import android.app.appsearch.util.ExceptionUtil;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.ArrayList;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -57,10 +58,11 @@
      * @param service The {@link IAppSearchManager} service from which to make api calls
      * @param userHandle The user for which the session should be created
      * @param callerAttributionSource The attribution source containing the caller's package name
-     *                                and uid
+     *     and uid
      * @param isForEnterprise Whether the session should query the user's enterprise profile
      */
-    ReadOnlyGlobalSearchSession(@NonNull IAppSearchManager service,
+    ReadOnlyGlobalSearchSession(
+            @NonNull IAppSearchManager service,
             @NonNull UserHandle userHandle,
             @NonNull AppSearchAttributionSource callerAttributionSource,
             boolean isForEnterprise) {
@@ -79,47 +81,51 @@
                     new InitializeAidlRequest(
                             mCallerAttributionSource,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime()),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime()),
                     new IAppSearchResultCallback.Stub() {
                         @Override
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<Void> result = resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    callback.accept(AppSearchResult.newSuccessfulResult(null));
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<Void> result = resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            callback.accept(
+                                                    AppSearchResult.newSuccessfulResult(null));
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
+                                        }
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
     /**
      * Retrieves {@link GenericDocument} documents, belonging to the specified package name and
-     * database name and identified by the namespace and ids in the request, from the
-     * {@link GlobalSearchSession} database.
+     * database name and identified by the namespace and ids in the request, from the {@link
+     * GlobalSearchSession} database.
      *
      * <p>If the package or database doesn't exist or if the calling package doesn't have access,
      * the gets will be handled as failures in an {@link AppSearchBatchResult} object in the
      * callback.
      *
-     * @param packageName  the name of the package to get from
+     * @param packageName the name of the package to get from
      * @param databaseName the name of the database to get from
-     * @param request      a request containing a namespace and IDs to get documents for.
-     * @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 IDs. The
-     *                     values are the returned {@link GenericDocument}s on success, or a failed
-     *                     {@link AppSearchResult} otherwise. IDs that are not found will return a
-     *                     failed {@link AppSearchResult} with a result code of
-     *                     {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
-     *                     occurs in the AppSearch service,
-     *                     {@link BatchResultCallback#onSystemError} will be invoked with a
-     *                     {@link Throwable}.
+     * @param request a request containing a namespace and IDs to get documents for.
+     * @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 IDs. The values are the
+     *     returned {@link GenericDocument}s on success, or a failed {@link AppSearchResult}
+     *     otherwise. IDs that are not found will return a failed {@link AppSearchResult} with a
+     *     result code of {@link AppSearchResult#RESULT_NOT_FOUND}. If an unexpected internal error
+     *     occurs in the AppSearch service, {@link BatchResultCallback#onSystemError} will be
+     *     invoked with a {@link Throwable}.
      */
     public void getByDocumentId(
             @NonNull String packageName,
@@ -135,18 +141,17 @@
 
         try {
             mService.getDocuments(
-                    mCallerAttributionSource,
-                    /*targetPackageName=*/packageName,
-                    databaseName,
-                    request.getNamespace(),
-                    new ArrayList<>(request.getIds()),
-                    request.getProjectionsInternal(),
-                    mUserHandle,
-                    /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                    mIsForEnterprise,
+                    new GetDocumentsAidlRequest(
+                            mCallerAttributionSource,
+                            /* targetPackageName= */ packageName,
+                            databaseName,
+                            request,
+                            mUserHandle,
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
+                            mIsForEnterprise),
                     SearchSessionUtil.createGetDocumentCallback(executor, callback));
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -165,16 +170,22 @@
      * SearchResults#getNextPage}.
      *
      * @param queryExpression query string to search.
-     * @param searchSpec      spec for setting document filters, adding projection, setting term
-     *                        match type, etc.
+     * @param searchSpec spec for setting document filters, adding projection, setting term match
+     *     type, etc.
      * @return a {@link SearchResults} object for retrieved matched documents.
      */
     @NonNull
     public SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
         Objects.requireNonNull(queryExpression);
         Objects.requireNonNull(searchSpec);
-        return new SearchResults(mService, mCallerAttributionSource, /*databaseName=*/null,
-                queryExpression, searchSpec, mUserHandle, mIsForEnterprise);
+        return new SearchResults(
+                mService,
+                mCallerAttributionSource,
+                /* databaseName= */ null,
+                queryExpression,
+                searchSpec,
+                mUserHandle,
+                mIsForEnterprise);
     }
 
     /**
@@ -185,11 +196,12 @@
      * <p>If the requested package/database combination does not exist or the caller has not been
      * granted access to it, then an empty GetSchemaResponse will be returned.
      *
-     * @param packageName  the package that owns the requested {@link AppSearchSchema} instances.
+     * @param packageName the package that owns the requested {@link AppSearchSchema} instances.
      * @param databaseName the database that owns the requested {@link AppSearchSchema} instances.
-     * @return The pending {@link GetSchemaResponse} containing the schemas that the caller has
-     *         access to or an empty GetSchemaResponse if the request package and database does not
-     *         exist, has not set a schema or contains no schemas that are accessible to the caller.
+     * @param executor Executor on which to invoke the callback.
+     * @param callback The pending {@link GetSchemaResponse} containing the schemas that the caller
+     *     has access to or an empty GetSchemaResponse if the request package and database does not
+     *     exist, has not set a schema or contains no schemas that are accessible to the caller.
      */
     public void getSchema(
             @NonNull String packageName,
@@ -207,35 +219,49 @@
                             packageName,
                             databaseName,
                             mUserHandle,
-                            /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
+                            /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
                             mIsForEnterprise),
                     new IAppSearchResultCallback.Stub() {
+                        @SuppressWarnings({"rawtypes", "unchecked"})
                         @Override
                         public void onResult(AppSearchResultParcel resultParcel) {
-                            safeExecute(executor, callback, () -> {
-                                AppSearchResult<GetSchemaResponse> result =
-                                        resultParcel.getResult();
-                                if (result.isSuccess()) {
-                                    GetSchemaResponse response = result.getResultValue();
-                                    callback.accept(AppSearchResult.newSuccessfulResult(response));
-                                } else {
-                                    callback.accept(AppSearchResult.newFailedResult(result));
-                                }
-                            });
+                            safeExecute(
+                                    executor,
+                                    callback,
+                                    () -> {
+                                        AppSearchResult<GetSchemaResponse> result =
+                                                resultParcel.getResult();
+                                        if (result.isSuccess()) {
+                                            GetSchemaResponse response = result.getResultValue();
+                                            callback.accept(
+                                                    AppSearchResult.newSuccessfulResult(response));
+                                        } else {
+                                            callback.accept(
+                                                    AppSearchResult.newFailedResult(result));
+                                        }
+                                    });
                         }
                     });
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
-    /** @hide */
+    /**
+     * Returns the service instance to make IPC calls.
+     *
+     * @hide
+     */
     @VisibleForTesting
     public IAppSearchManager getService() {
         return mService;
     }
 
-    /** @hide */
+    /**
+     * Returns if session supports Enterprise flow.
+     *
+     * @hide
+     */
     @VisibleForTesting
     public boolean isForEnterprise() {
         return mIsForEnterprise;
diff --git a/framework/java/android/app/appsearch/SearchResults.java b/framework/java/android/app/appsearch/SearchResults.java
index 08a9a25..6b6aa0a 100644
--- a/framework/java/android/app/appsearch/SearchResults.java
+++ b/framework/java/android/app/appsearch/SearchResults.java
@@ -31,6 +31,7 @@
 import android.app.appsearch.aidl.IAppSearchResultCallback;
 import android.app.appsearch.aidl.InvalidateNextPageTokenAidlRequest;
 import android.app.appsearch.aidl.SearchAidlRequest;
+import android.app.appsearch.util.ExceptionUtil;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -67,8 +68,7 @@
     private final AppSearchAttributionSource mAttributionSource;
 
     // The database name to search over. If null, this will search over all database names.
-    @Nullable
-    private final String mDatabaseName;
+    @Nullable private final String mDatabaseName;
 
     private final String mQueryExpression;
 
@@ -125,13 +125,23 @@
                 if (mDatabaseName == null) {
                     // Global search, there's no one package-database combination to check.
                     mService.globalSearch(
-                            new GlobalSearchAidlRequest(mAttributionSource, mQueryExpression,
-                                    mSearchSpec, mUserHandle, binderCallStartTimeMillis,
-                                    mIsForEnterprise), wrapCallback(executor, callback));
+                            new GlobalSearchAidlRequest(
+                                    mAttributionSource,
+                                    mQueryExpression,
+                                    mSearchSpec,
+                                    mUserHandle,
+                                    binderCallStartTimeMillis,
+                                    mIsForEnterprise),
+                            wrapCallback(executor, callback));
                 } else {
                     // Normal local search, pass in specified database.
-                    mService.search(new SearchAidlRequest(mAttributionSource, mDatabaseName,
-                                    mQueryExpression, mSearchSpec, mUserHandle,
+                    mService.search(
+                            new SearchAidlRequest(
+                                    mAttributionSource,
+                                    mDatabaseName,
+                                    mQueryExpression,
+                                    mSearchSpec,
+                                    mUserHandle,
                                     binderCallStartTimeMillis),
                             wrapCallback(executor, callback));
                 }
@@ -139,16 +149,23 @@
                 // TODO(b/276349029): Log different join types when they get added.
                 @AppSearchSchema.StringPropertyConfig.JoinableValueType
                 int joinType = JOINABLE_VALUE_TYPE_NONE;
-                if (mSearchSpec.getJoinSpec() != null
-                        && !mSearchSpec.getJoinSpec().getChildPropertyExpression().isEmpty()) {
+                JoinSpec joinSpec = mSearchSpec.getJoinSpec();
+                if (joinSpec != null && !joinSpec.getChildPropertyExpression().isEmpty()) {
                     joinType = JOINABLE_VALUE_TYPE_QUALIFIED_ID;
                 }
-                mService.getNextPage(new GetNextPageAidlRequest(mAttributionSource, mDatabaseName,
-                        mNextPageToken, joinType, mUserHandle, binderCallStartTimeMillis,
-                        mIsForEnterprise), wrapCallback(executor, callback));
+                mService.getNextPage(
+                        new GetNextPageAidlRequest(
+                                mAttributionSource,
+                                mDatabaseName,
+                                mNextPageToken,
+                                joinType,
+                                mUserHandle,
+                                binderCallStartTimeMillis,
+                                mIsForEnterprise),
+                        wrapCallback(executor, callback));
             }
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            ExceptionUtil.handleRemoteException(e);
         }
     }
 
@@ -156,10 +173,13 @@
     public void close() {
         if (!mIsClosed) {
             try {
-                mService.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
-                        mAttributionSource, mNextPageToken, mUserHandle,
-                        /*binderCallStartTimeMillis=*/ SystemClock.elapsedRealtime(),
-                        mIsForEnterprise));
+                mService.invalidateNextPageToken(
+                        new InvalidateNextPageTokenAidlRequest(
+                                mAttributionSource,
+                                mNextPageToken,
+                                mUserHandle,
+                                /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
+                                mIsForEnterprise));
                 mIsClosed = true;
             } catch (RemoteException e) {
                 Log.e(TAG, "Unable to close the SearchResults", e);
@@ -186,11 +206,10 @@
             @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
         if (searchResultPageResult.isSuccess()) {
             try {
-                SearchResultPage searchResultPage = Objects.requireNonNull(
-                        searchResultPageResult.getResultValue());
+                SearchResultPage searchResultPage =
+                        Objects.requireNonNull(searchResultPageResult.getResultValue());
                 mNextPageToken = searchResultPage.getNextPageToken();
-                callback.accept(AppSearchResult.newSuccessfulResult(
-                        searchResultPage.getResults()));
+                callback.accept(AppSearchResult.newSuccessfulResult(searchResultPage.getResults()));
             } catch (RuntimeException e) {
                 callback.accept(AppSearchResult.throwableToFailedResult(e));
             }
diff --git a/framework/java/android/app/appsearch/SearchSessionUtil.java b/framework/java/android/app/appsearch/SearchSessionUtil.java
index ed4a32f..474ad8f 100644
--- a/framework/java/android/app/appsearch/SearchSessionUtil.java
+++ b/framework/java/android/app/appsearch/SearchSessionUtil.java
@@ -33,23 +33,22 @@
 import java.util.function.Consumer;
 
 /**
- * @hide
  * Contains util methods used in both {@link GlobalSearchSession} and {@link AppSearchSession}.
+ *
+ * @hide
  */
 public class SearchSessionUtil {
     private static final String TAG = "AppSearchSessionUtil";
 
-    /**
-     * Constructor for in case we create an instance
-     */
+    /** Constructor for in case we create an instance */
     private SearchSessionUtil() {}
 
     /**
      * Calls {@link BatchResultCallback#onSystemError} with a throwable derived from the given
      * failed {@link AppSearchResult}.
      *
-     * <p>The {@link AppSearchResult} generally comes from
-     * {@link IAppSearchBatchResultCallback#onSystemError}.
+     * <p>The {@link AppSearchResult} generally comes from {@link
+     * IAppSearchBatchResultCallback#onSystemError}.
      *
      * <p>This method should be called from the callback executor thread.
      *
@@ -59,8 +58,9 @@
     public static void sendSystemErrorToCallback(
             @NonNull AppSearchResult<?> failedResult, @NonNull BatchResultCallback<?, ?> callback) {
         Preconditions.checkArgument(!failedResult.isSuccess());
-        Throwable throwable = new AppSearchException(
-                failedResult.getResultCode(), failedResult.getErrorMessage());
+        Throwable throwable =
+                new AppSearchException(
+                        failedResult.getResultCode(), failedResult.getErrorMessage());
         callback.onSystemError(throwable);
     }
 
@@ -75,8 +75,8 @@
      * errorCallback synchronously on the calling thread.
      *
      * @param executor The executor on which to safely execute the lambda
-     * @param errorCallback The callback to trigger with a failed {@link AppSearchResult} if
-     *                      the {@link Executor#execute} call fails.
+     * @param errorCallback The callback to trigger with a failed {@link AppSearchResult} if the
+     *     {@link Executor#execute} call fails.
      * @param runnable The lambda to execute on the executor
      */
     public static <T> void safeExecute(
@@ -102,8 +102,8 @@
      * errorCallback synchronously on the calling thread.
      *
      * @param executor The executor on which to safely execute the lambda
-     * @param errorCallback The callback to trigger with a failed {@link AppSearchResult} if
-     *                      the {@link Executor#execute} call fails.
+     * @param errorCallback The callback to trigger with a failed {@link AppSearchResult} if the
+     *     {@link Executor#execute} call fails.
      * @param runnable The lambda to execute on the executor
      */
     public static void safeExecute(
@@ -130,46 +130,58 @@
             @NonNull BatchResultCallback<String, GenericDocument> callback) {
         return new IAppSearchBatchResultCallback.Stub() {
             @Override
+            @SuppressWarnings({"unchecked", "rawtypes"})
             public void onResult(AppSearchBatchResultParcel resultParcel) {
-                safeExecute(executor, callback, () -> {
-                    AppSearchBatchResult<String, GenericDocumentParcel> result =
-                            resultParcel.getResult();
-                    AppSearchBatchResult.Builder<String, GenericDocument>
-                            documentResultBuilder =
-                            new AppSearchBatchResult.Builder<>();
+                safeExecute(
+                        executor,
+                        callback,
+                        () -> {
+                            AppSearchBatchResult<String, GenericDocumentParcel> result =
+                                    resultParcel.getResult();
+                            AppSearchBatchResult.Builder<String, GenericDocument>
+                                    documentResultBuilder = new AppSearchBatchResult.Builder<>();
 
-                    for (Map.Entry<String, GenericDocumentParcel> entry :
-                            result.getSuccesses().entrySet()) {
-                        GenericDocument document;
-                        try {
-                            document = new GenericDocument(entry.getValue());
-                        } catch (RuntimeException e) {
-                            documentResultBuilder.setFailure(
-                                    entry.getKey(),
-                                    AppSearchResult.RESULT_INTERNAL_ERROR,
-                                    e.getMessage());
-                            continue;
-                        }
-                        documentResultBuilder.setSuccess(
-                                entry.getKey(), document);
-                    }
+                            for (Map.Entry<String, GenericDocumentParcel> entry :
+                                    result.getSuccesses().entrySet()) {
+                                GenericDocument document;
+                                try {
+                                    GenericDocumentParcel genericDocumentParcel = entry.getValue();
+                                    if (genericDocumentParcel == null) {
+                                        documentResultBuilder.setFailure(
+                                                entry.getKey(),
+                                                AppSearchResult.RESULT_INTERNAL_ERROR,
+                                                "Received null GenericDocumentParcel in"
+                                                        + " getByDocumentId API");
+                                        continue;
+                                    }
+                                    document = new GenericDocument(genericDocumentParcel);
+                                } catch (RuntimeException e) {
+                                    documentResultBuilder.setFailure(
+                                            entry.getKey(),
+                                            AppSearchResult.RESULT_INTERNAL_ERROR,
+                                            e.getMessage());
+                                    continue;
+                                }
+                                documentResultBuilder.setSuccess(entry.getKey(), document);
+                            }
 
-                    for (Entry<String, AppSearchResult<GenericDocumentParcel>> entry :
-                            result.getFailures().entrySet()) {
-                        documentResultBuilder.setFailure(
-                                entry.getKey(),
-                                entry.getValue().getResultCode(),
-                                entry.getValue().getErrorMessage());
-                    }
-                    callback.onResult(documentResultBuilder.build());
-
-                });
+                            for (Entry<String, AppSearchResult<GenericDocumentParcel>> entry :
+                                    result.getFailures().entrySet()) {
+                                documentResultBuilder.setFailure(
+                                        entry.getKey(),
+                                        entry.getValue().getResultCode(),
+                                        entry.getValue().getErrorMessage());
+                            }
+                            callback.onResult(documentResultBuilder.build());
+                        });
             }
 
             @Override
+            @SuppressWarnings({"unchecked", "rawtypes"})
             public void onSystemError(AppSearchResultParcel result) {
                 safeExecute(
-                        executor, callback,
+                        executor,
+                        callback,
                         () -> sendSystemErrorToCallback(result.getResult(), callback));
             }
         };
diff --git a/framework/java/android/app/appsearch/aidl/AppSearchAttributionSource.java b/framework/java/android/app/appsearch/aidl/AppSearchAttributionSource.java
index c2ecb9d..94382cd 100644
--- a/framework/java/android/app/appsearch/aidl/AppSearchAttributionSource.java
+++ b/framework/java/android/app/appsearch/aidl/AppSearchAttributionSource.java
@@ -38,10 +38,9 @@
 /**
  * Compatibility version of AttributionSource.
  *
- * Refactor AttributionSource to work on older API levels. For Android S+, this class maintains the
- * original implementation of AttributionSource methods. However, for Android R-, this class
- * creates a new implementation.
- * Replace calls to AttributionSource with AppSearchAttributionSource.
+ * <p>Refactor AttributionSource to work on older API levels. For Android S+, this class maintains
+ * the original implementation of AttributionSource methods. However, for Android R-, this class
+ * creates a new implementation. Replace calls to AttributionSource with AppSearchAttributionSource.
  * For a given Context, replace calls to getAttributionSource with createAttributionSource.
  *
  * @hide
@@ -49,79 +48,108 @@
 @SafeParcelable.Class(creator = "AppSearchAttributionSourceCreator")
 public final class AppSearchAttributionSource extends AbstractSafeParcelable {
     @NonNull
-    public static final AppSearchAttributionSourceCreator CREATOR =
-        new AppSearchAttributionSourceCreator();
+    public static final Parcelable.Creator<AppSearchAttributionSource> CREATOR =
+            new AppSearchAttributionSourceCreator();
 
-    @NonNull
-    private final Compat mCompat;
+    @NonNull private final Compat mCompat;
 
     @Nullable
     @Field(id = 1, getter = "getAttributionSource")
     private final AttributionSource mAttributionSource;
-    @Nullable
+
+    @NonNull
     @Field(id = 2, getter = "getPackageName")
     private final String mCallingPackageName;
+
     @Field(id = 3, getter = "getUid")
     private final int mCallingUid;
 
+    @Field(id = 4, getter = "getPid")
+    private int mCallingPid;
+
+    private static final int INVALID_PID = -1;
+
     /**
      * Constructs an instance of AppSearchAttributionSource for AbstractSafeParcelable.
-     * @param attributionSource The attribution source that is accessing permission
-     *      protected data.
+     *
+     * @param attributionSource The attribution source that is accessing permission protected data.
      * @param callingPackageName The package that is accessing the permission protected data.
      * @param callingUid The UID that is accessing the permission protected data.
      */
     @Constructor
     AppSearchAttributionSource(
-        @Param(id = 1) @Nullable AttributionSource attributionSource,
-        @Param(id = 2) @Nullable String callingPackageName,
-        @Param(id = 3) int callingUid) {
+            @Param(id = 1) @Nullable AttributionSource attributionSource,
+            @Param(id = 2) @NonNull String callingPackageName,
+            @Param(id = 3) int callingUid,
+            @Param(id = 4) int callingPid) {
         mAttributionSource = attributionSource;
-        mCallingPackageName = callingPackageName;
+        mCallingPackageName = Objects.requireNonNull(callingPackageName);
         mCallingUid = callingUid;
+        mCallingPid = callingPid;
         if (VERSION.SDK_INT >= Build.VERSION_CODES.S && mAttributionSource != null) {
-            mCompat = new Api31Impl(mAttributionSource);
+            mCompat = new Api31Impl(mAttributionSource, mCallingPid);
         } else {
-            mCompat = new Api19Impl(mCallingPackageName, mCallingUid);
+            // If this object is being constructed as part of a oneway Binder call, getCallingPid
+            // will return 0 instead of the true PID. In that case, invalidate the PID by setting it
+            // to INVALID_PID (-1).
+            final int callingPidFromBinder = Binder.getCallingPid();
+            if (callingPidFromBinder == 0) {
+                mCallingPid = INVALID_PID;
+            }
+            Api19Impl impl = new Api19Impl(mCallingPackageName, mCallingUid, mCallingPid);
+            impl.enforceCallingUid();
+            impl.enforceCallingPid();
+            mCompat = impl;
         }
     }
 
     /**
      * Constructs an instance of AppSearchAttributionSource.
-     * @param compat The compat version that provides AttributionSource implementation on
-     *      lower API levels.
+     *
+     * @param compat The compat version that provides AttributionSource implementation on lower API
+     *     levels.
      */
     private AppSearchAttributionSource(@NonNull Compat compat) {
         mCompat = Objects.requireNonNull(compat);
         mAttributionSource = mCompat.getAttributionSource();
         mCallingPackageName = mCompat.getPackageName();
         mCallingUid = mCompat.getUid();
+        mCallingPid = mCompat.getPid();
     }
 
     /**
      * Constructs an instance of AppSearchAttributionSource for testing.
+     *
      * @param callingPackageName The package that is accessing the permission protected data.
      * @param callingUid The UID that is accessing the permission protected data.
      */
     @VisibleForTesting
-    public AppSearchAttributionSource(@Nullable String callingPackageName, int callingUid) {
-        mCallingPackageName = callingPackageName;
+    public AppSearchAttributionSource(
+            @NonNull String callingPackageName, int callingUid, int callingPid) {
+        mCallingPackageName = Objects.requireNonNull(callingPackageName);
         mCallingUid = callingUid;
+        mCallingPid = callingPid;
 
         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-             mAttributionSource = new AttributionSource.Builder(mCallingUid)
-                    .setPackageName(mCallingPackageName).build();
-            mCompat = new Api31Impl(mAttributionSource);
+            // This constructor is only used in unit test, AttributionSource#setPid is only
+            // available on 34+.
+            AttributionSource.Builder attributionSourceBuilder =
+                    new AttributionSource.Builder(mCallingUid).setPackageName(mCallingPackageName);
+            if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                attributionSourceBuilder.setPid(callingPid);
+            }
+            mAttributionSource = attributionSourceBuilder.build();
+            mCompat = new Api31Impl(mAttributionSource, mCallingPid);
         } else {
             mAttributionSource = null;
-            mCompat = new Api19Impl(mCallingPackageName, mCallingUid);
+            mCompat = new Api19Impl(mCallingPackageName, mCallingUid, mCallingPid);
         }
     }
 
     /**
      * Provides a backward-compatible wrapper for AttributionSource.
      *
-     * This method is not supported on devices running SDK <= 30(R) since the AttributionSource
+     * <p>This method is not supported on devices running SDK <= 30(R) since the AttributionSource
      * class will not be available.
      *
      * @param attributionSource AttributionSource class to wrap, must not be null
@@ -130,14 +158,14 @@
     @RequiresApi(Build.VERSION_CODES.S)
     @NonNull
     private static AppSearchAttributionSource toAppSearchAttributionSource(
-        @NonNull AttributionSource attributionSource) {
-        return new AppSearchAttributionSource(new Api31Impl(attributionSource));
+            @NonNull AttributionSource attributionSource, int pid) {
+        return new AppSearchAttributionSource(new Api31Impl(attributionSource, pid));
     }
 
     /**
      * Provides a backward-compatible wrapper for AttributionSource.
      *
-     * This method is not supported on devices running SDK <= 19(H) since the AttributionSource
+     * <p>This method is not supported on devices running SDK <= 19(H) since the AttributionSource
      * class will not be available.
      *
      * @param packageName The package name to wrap, must not be null
@@ -145,9 +173,8 @@
      * @return wrapped class
      */
     private static AppSearchAttributionSource toAppSearchAttributionSource(
-        @Nullable String packageName, int uid) {
-        return new AppSearchAttributionSource(
-            new Api19Impl(packageName, uid));
+            @NonNull String packageName, int uid, int pid) {
+        return new AppSearchAttributionSource(new Api19Impl(packageName, uid, pid));
     }
 
     /**
@@ -156,23 +183,21 @@
      * @param context Context the application is running on.
      */
     public static AppSearchAttributionSource createAttributionSource(
-        @NonNull Context context) {
+            @NonNull Context context, int callingPid) {
         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            return toAppSearchAttributionSource(context.getAttributionSource());
+            return toAppSearchAttributionSource(context.getAttributionSource(), callingPid);
         }
 
-        return toAppSearchAttributionSource(context.getPackageName(), Process.myUid());
+        return toAppSearchAttributionSource(context.getPackageName(), Process.myUid(), callingPid);
     }
 
-    /**
-     * Return AttributionSource on Android S+ and return null on Android R-.
-     */
+    /** Return AttributionSource on Android S+ and return null on Android R-. */
     @Nullable
     public AttributionSource getAttributionSource() {
         return mCompat.getAttributionSource();
     }
 
-    @Nullable
+    @NonNull
     public String getPackageName() {
         return mCompat.getPackageName();
     }
@@ -181,11 +206,15 @@
         return mCompat.getUid();
     }
 
+    public int getPid() {
+        return mCompat.getPid();
+    }
+
     @Override
     public int hashCode() {
         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            AttributionSource attributionSource = Objects.requireNonNull(
-                mCompat.getAttributionSource());
+            AttributionSource attributionSource =
+                    Objects.requireNonNull(mCompat.getAttributionSource());
             return attributionSource.hashCode();
         }
 
@@ -200,15 +229,17 @@
 
         AppSearchAttributionSource that = (AppSearchAttributionSource) o;
         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            AttributionSource thisAttributionSource = Objects.requireNonNull(
-                mCompat.getAttributionSource());
-            AttributionSource thatAttributionSource = Objects.requireNonNull(
-                that.getAttributionSource());
-            return thisAttributionSource.equals(thatAttributionSource);
+            AttributionSource thisAttributionSource =
+                    Objects.requireNonNull(mCompat.getAttributionSource());
+            AttributionSource thatAttributionSource =
+                    Objects.requireNonNull(that.getAttributionSource());
+            return thisAttributionSource.equals(thatAttributionSource)
+                    && (that.getPid() == mCompat.getPid());
         }
 
         return (Objects.equals(mCompat.getPackageName(), that.getPackageName())
-            && (mCompat.getUid() == that.getUid()));
+                && (mCompat.getUid() == that.getUid())
+                && mCompat.getPid() == that.getPid());
     }
 
     @Override
@@ -219,7 +250,7 @@
     /** Compat class for AttributionSource to provide implementation for lower API levels. */
     private interface Compat {
         /** The package that is accessing the permission protected data. */
-        @Nullable
+        @NonNull
         String getPackageName();
 
         /** The attribution source of the app accessing the permission protected data. */
@@ -228,27 +259,39 @@
 
         /** The UID that is accessing the permission protected data. */
         int getUid();
+
+        /** The PID that is accessing the permission protected data. */
+        int getPid();
     }
 
     @RequiresApi(VERSION_CODES.S)
     private static final class Api31Impl implements Compat {
 
         private final AttributionSource mAttributionSource;
+        private final int mPid;
 
         /**
          * Creates a new implementation for AppSearchAttributionSource's Compat for API levels 31+.
          *
-         * @param attributionSource The attribution source that is accessing permission
-         *      protected data.
+         * @param attributionSource The attribution source that is accessing permission protected
+         *     data.
          */
-        Api31Impl(@NonNull AttributionSource attributionSource) {
+        Api31Impl(@NonNull AttributionSource attributionSource, int pid) {
             mAttributionSource = attributionSource;
+            mPid = pid;
         }
 
         @Override
-        @Nullable
+        @NonNull
         public String getPackageName() {
-            return mAttributionSource.getPackageName();
+            // The {@link AttributionSource} in the constructor is set using
+            // {@link Context#getAttributionSource} and not using the Builder. The
+            // packageName returned from {@link AttributionSource#getPackageName} can be null as
+            // AttributionSource can use either uid and package name to determine who has access
+            // to the data, so either one of them can be null but not both. It is a common practice
+            // to use {@link AttributionSource#getPackageName} without any known issues/bugs. If
+            // we ever receive a null here we will throw a NullPointerException.
+            return Objects.requireNonNull(mAttributionSource.getPackageName());
         }
 
         @Nullable
@@ -261,12 +304,18 @@
         public int getUid() {
             return mAttributionSource.getUid();
         }
+
+        @Override
+        public int getPid() {
+            return mPid;
+        }
     }
 
     private static class Api19Impl implements Compat {
 
-        @Nullable private final String mPackageName;
+        @NonNull private final String mPackageName;
         private final int mUid;
+        private final int mPid;
 
         /**
          * Creates a new implementation for AppSearchAttributionSource's Compat for API levels 19+.
@@ -274,13 +323,14 @@
          * @param packageName The package name that is accessing permission protected data.
          * @param uid The uid that is accessing permission protected data.
          */
-        Api19Impl(@Nullable String packageName, int uid) {
-            mPackageName = packageName;
+        Api19Impl(@NonNull String packageName, int uid, int pid) {
+            mPackageName = Objects.requireNonNull(packageName);
             mUid = uid;
+            mPid = pid;
         }
 
         @Override
-        @Nullable
+        @NonNull
         public String getPackageName() {
             return mPackageName;
         }
@@ -300,19 +350,24 @@
             return mUid;
         }
 
+        @Override
+        public int getPid() {
+            return mPid;
+        }
+
         /**
          * If you are handling an IPC and you don't trust the caller you need to validate whether
          * the attribution source is one for the calling app to prevent the caller to pass you a
          * source from another app without including themselves in the attribution chain.
          *
-         * @throws SecurityException if the attribution source cannot be trusted to be from
-         *         the caller.
+         * @throws SecurityException if the attribution source cannot be trusted to be from the
+         *     caller.
          */
         private void enforceCallingUid() {
             if (!checkCallingUid()) {
                 int callingUid = Binder.getCallingUid();
                 throw new SecurityException(
-                    "Calling uid: " + callingUid + " doesn't match source uid: " + mUid);
+                        "Calling uid: " + callingUid + " doesn't match source uid: " + mUid);
             }
             // The verification for calling package happens in the service during API call.
         }
@@ -334,29 +389,38 @@
         }
 
         /**
-         * Validate that the call is happening on a Binder transaction.
+         * Validate that the pid being claimed for the calling app is not spoofed.
          *
-         * @throws SecurityException if the attribution source cannot be trusted to be from
-         *         the caller.
+         * <p>Note that the PID may be unavailable, for example if we're in a oneway Binder call. In
+         * this case, calling enforceCallingPid is guaranteed to fail. The caller should anticipate
+         * this.
+         *
+         * @throws SecurityException if the attribution source cannot be trusted to be from the
+         *     caller.
          */
         private void enforceCallingPid() {
             if (!checkCallingPid()) {
-                throw new SecurityException(
-                    "Calling pid: "
-                        + Binder.getCallingPid()
-                        + " is same as process pid: "
-                        + Process.myPid());
+                if (Binder.getCallingPid() == 0) {
+                    throw new SecurityException(
+                            "Calling pid unavailable due to oneway Binder " + "call.");
+                } else {
+                    throw new SecurityException(
+                            "Calling pid: "
+                                    + Binder.getCallingPid()
+                                    + " doesn't match source pid: "
+                                    + mPid);
+                }
             }
         }
 
         /**
-         * Validate that the call is happening on a Binder transaction.
+         * Validate that the pid being claimed for the calling app is not spoofed
          *
-         * @return if the call is happening on the Binder thread.
+         * @return if the attribution source cannot be trusted to be from the caller.
          */
         private boolean checkCallingPid() {
             final int callingPid = Binder.getCallingPid();
-            if (callingPid == Process.myPid()) {
+            if (mPid != INVALID_PID && mPid != callingPid) {
                 // Only call this on the binder thread. If a new thread is created to handle the
                 // client request, Binder.getCallingPid() will return the thread's own pid.
                 return false;
@@ -364,4 +428,4 @@
             return true;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/framework/java/android/app/appsearch/aidl/AppSearchBatchResultParcel.java b/framework/java/android/app/appsearch/aidl/AppSearchBatchResultParcel.java
index 3bc877f..bec3400 100644
--- a/framework/java/android/app/appsearch/aidl/AppSearchBatchResultParcel.java
+++ b/framework/java/android/app/appsearch/aidl/AppSearchBatchResultParcel.java
@@ -22,9 +22,11 @@
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.ParcelableUtil;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Bundle;
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import java.util.Map;
 import java.util.Objects;
@@ -42,8 +44,10 @@
  */
 @SafeParcelable.Class(creator = "AppSearchBatchResultParcelCreator", creatorIsFinal = false)
 public final class AppSearchBatchResultParcel<ValueType> extends AbstractSafeParcelable {
+
     @NonNull
-    public static final AppSearchBatchResultParcelCreator CREATOR =
+    @SuppressWarnings("rawtypes")
+    public static final Parcelable.Creator<AppSearchBatchResultParcel> CREATOR =
             new AppSearchBatchResultParcelCreator() {
                 @Override
                 public AppSearchBatchResultParcel createFromParcel(Parcel in) {
@@ -55,7 +59,7 @@
                         int size = unmarshallParcel.dataSize();
                         Bundle inputBundle = new Bundle();
                         while (unmarshallParcel.dataPosition() < size) {
-                            String key = unmarshallParcel.readString();
+                            String key = Objects.requireNonNull(unmarshallParcel.readString());
                             AppSearchResultParcel appSearchResultParcel =
                                     AppSearchResultParcel.directlyReadFromParcel(unmarshallParcel);
                             inputBundle.putParcelable(key, appSearchResultParcel);
@@ -72,34 +76,70 @@
     @NonNull
     final Bundle mAppSearchResultBundle;
 
-    @Nullable
-    private AppSearchBatchResult<String, ValueType> mResultCached;
+    @Nullable private AppSearchBatchResult<String, ValueType> mResultCached;
 
     @Constructor
-    AppSearchBatchResultParcel(
-            @Param(id = 1) Bundle appSearchResultBundle) {
+    AppSearchBatchResultParcel(@Param(id = 1) Bundle appSearchResultBundle) {
         mAppSearchResultBundle = appSearchResultBundle;
     }
 
-    /** Creates a new {@link AppSearchBatchResultParcel} from the given result. */
-    public AppSearchBatchResultParcel(@NonNull AppSearchBatchResult<String, ValueType> result) {
-        mResultCached = result;
-        mAppSearchResultBundle = new Bundle();
-        for (Map.Entry<String, AppSearchResult<ValueType>> entry
-                : result.getAll().entrySet()) {
-            mAppSearchResultBundle.putParcelable(entry.getKey(),
-                    new AppSearchResultParcel<>(entry.getValue()));
+    /**
+     * Creates a new {@link AppSearchBatchResultParcel} from the given {@link GenericDocumentParcel}
+     * results.
+     */
+    @SuppressWarnings("unchecked")
+    public static AppSearchBatchResultParcel<GenericDocumentParcel>
+            fromStringToGenericDocumentParcel(
+                    @NonNull AppSearchBatchResult<String, GenericDocumentParcel> result) {
+        Bundle appSearchResultBundle = new Bundle();
+        for (Map.Entry<String, AppSearchResult<GenericDocumentParcel>> entry :
+                result.getAll().entrySet()) {
+            AppSearchResultParcel<GenericDocumentParcel> appSearchResultParcel;
+            // Create result from value in success case and errorMessage in
+            // failure case.
+            if (entry.getValue().isSuccess()) {
+                GenericDocumentParcel genericDocumentParcel =
+                        Objects.requireNonNull(entry.getValue().getResultValue());
+                appSearchResultParcel =
+                        AppSearchResultParcel.fromGenericDocumentParcel(genericDocumentParcel);
+            } else {
+                appSearchResultParcel = AppSearchResultParcel.fromFailedResult(entry.getValue());
+            }
+            appSearchResultBundle.putParcelable(entry.getKey(), appSearchResultParcel);
         }
+        return new AppSearchBatchResultParcel<>(appSearchResultBundle);
+    }
+
+    /** Creates a new {@link AppSearchBatchResultParcel} from the given {@link Void} results. */
+    @SuppressWarnings("unchecked")
+    public static AppSearchBatchResultParcel<Void> fromStringToVoid(
+            @NonNull AppSearchBatchResult<String, Void> result) {
+        Bundle appSearchResultBundle = new Bundle();
+        for (Map.Entry<String, AppSearchResult<Void>> entry : result.getAll().entrySet()) {
+            AppSearchResultParcel<Void> appSearchResultParcel;
+            // Create result from value in success case and errorMessage in
+            // failure case.
+            if (entry.getValue().isSuccess()) {
+                appSearchResultParcel = AppSearchResultParcel.fromVoid();
+            } else {
+                appSearchResultParcel = AppSearchResultParcel.fromFailedResult(entry.getValue());
+            }
+            appSearchResultBundle.putParcelable(entry.getKey(), appSearchResultParcel);
+        }
+        return new AppSearchBatchResultParcel<>(appSearchResultBundle);
     }
 
     @NonNull
+    @SuppressWarnings("unchecked")
     public AppSearchBatchResult<String, ValueType> getResult() {
         if (mResultCached == null) {
             AppSearchBatchResult.Builder<String, ValueType> builder =
                     new AppSearchBatchResult.Builder<>();
             for (String key : mAppSearchResultBundle.keySet()) {
-                builder.setResult(key, mAppSearchResultBundle
-                        .getParcelable(key, AppSearchResultParcel.class)
+                builder.setResult(
+                        key,
+                        mAppSearchResultBundle
+                                .getParcelable(key, AppSearchResultParcel.class)
                                 .getResult());
             }
             mResultCached = builder.build();
@@ -109,6 +149,7 @@
 
     /** @hide */
     @Override
+    @SuppressWarnings("unchecked")
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         byte[] bytes;
         // Create a parcel object to serialize results. So that we can use Parcel.writeBlob() to
diff --git a/framework/java/android/app/appsearch/aidl/AppSearchResultParcel.java b/framework/java/android/app/appsearch/aidl/AppSearchResultParcel.java
index fcc98e6..08d6309 100644
--- a/framework/java/android/app/appsearch/aidl/AppSearchResultParcel.java
+++ b/framework/java/android/app/appsearch/aidl/AppSearchResultParcel.java
@@ -19,17 +19,30 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.GetSchemaResponse;
+import android.app.appsearch.InternalSetSchemaResponse;
 import android.app.appsearch.ParcelableUtil;
+import android.app.appsearch.SearchResultPage;
+import android.app.appsearch.SearchSuggestionResult;
+import android.app.appsearch.SetSchemaResponse.MigrationFailure;
+import android.app.appsearch.StorageInfo;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.functions.ExecuteAppFunctionResponse;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.List;
+import java.util.Objects;
 
 /**
  * Parcelable wrapper around {@link AppSearchResult}.
  *
  * <p>{@link AppSearchResult} can contain any value, including non-parcelable values. For the
- * specific case of sending {@link AppSearchResult} across Binder, this class wraps an
- * {@link AppSearchResult} that contains a parcelable type and provides parcelability of the whole
+ * specific case of sending {@link AppSearchResult} across Binder, this class wraps an {@link
+ * AppSearchResult} that contains a parcelable type and provides parcelability of the whole
  * structure.
  *
  * @param <ValueType> The type of result object for successful calls. Must be a parcelable type.
@@ -39,13 +52,14 @@
 public final class AppSearchResultParcel<ValueType> extends AbstractSafeParcelable {
 
     @NonNull
-    public static final AppSearchResultParcelCreator CREATOR =
+    @SuppressWarnings("rawtypes")
+    public static final Parcelable.Creator<AppSearchResultParcel> CREATOR =
             new AppSearchResultParcelCreator() {
                 @Override
                 public AppSearchResultParcel createFromParcel(Parcel in) {
                     // We pass the result we get from ParcelableUtil#readBlob to
                     // AppSearchResultParcelCreator to decode.
-                    byte[] dataBlob = ParcelableUtil.readBlob(in);
+                    byte[] dataBlob = Objects.requireNonNull(ParcelableUtil.readBlob(in));
                     // Create a parcel object to un-serialize the byte array we are reading from
                     // Parcel.readBlob(). Parcel.WriteBlob() could take care of whether to pass
                     // data via binder directly or Android shared memory if the data is large.
@@ -61,44 +75,259 @@
             };
 
     @NonNull
-    private static final AppSearchResultParcelCreator CREATOR_WITHOUT_BLOB =
+    private static final Parcelable.Creator<AppSearchResultParcel> CREATOR_WITHOUT_BLOB =
             new AppSearchResultParcelCreator();
 
     @Field(id = 1)
-    final int mResultCode;
+    @AppSearchResult.ResultCode
+    int mResultCode;
+
     @Field(id = 2)
-    @Nullable final ValueParcel mValue;
+    @Nullable
+    String mErrorMessage;
+
     @Field(id = 3)
-    @Nullable final String mErrorMessage;
+    @Nullable
+    InternalSetSchemaResponse mInternalSetSchemaResponse;
+
+    @Field(id = 4)
+    @Nullable
+    GetSchemaResponse mGetSchemaResponse;
+
+    @Field(id = 5)
+    @Nullable
+    List<String> mStrings;
+
+    @Field(id = 6)
+    @Nullable
+    GenericDocumentParcel mGenericDocumentParcel;
+
+    @Field(id = 7)
+    @Nullable
+    SearchResultPage mSearchResultPage;
+
+    @Field(id = 8)
+    @Nullable
+    List<MigrationFailure> mMigrationFailures;
+
+    @Field(id = 9)
+    @Nullable
+    List<SearchSuggestionResult> mSearchSuggestionResults;
+
+    @Field(id = 10)
+    @Nullable
+    StorageInfo mStorageInfo;
+
+    @Field(id = 11)
+    @Nullable
+    ExecuteAppFunctionResponse mExecuteAppFunctionResponse;
 
     @NonNull AppSearchResult<ValueType> mResultCached;
 
+    /**
+     * Creates an AppSearchResultParcel for given value type.
+     *
+     * @param resultCode A {@link AppSearchResult} result code for {@link IAppSearchManager} API
+     *     response.
+     * @param errorMessage An error message in case of a failed response.
+     * @param internalSetSchemaResponse An {@link InternalSetSchemaResponse} type response.
+     * @param getSchemaResponse An {@link GetSchemaResponse} type response.
+     * @param strings An {@link List<String>} type response.
+     * @param genericDocumentParcel An {@link GenericDocumentParcel} type response.
+     * @param searchResultPage An {@link SearchResultPage} type response.
+     * @param migrationFailures An {@link List<MigrationFailure>} type response.
+     * @param searchSuggestionResults An {@link List<SearchSuggestionResult>} type response.
+     * @param storageInfo {@link StorageInfo} type response.
+     */
     @Constructor
     AppSearchResultParcel(
             @Param(id = 1) @AppSearchResult.ResultCode int resultCode,
-            @Param(id = 2) @Nullable ValueParcel<ValueType> value,
-            @Param(id = 3) @Nullable String errorMessage) {
+            @Param(id = 2) @Nullable String errorMessage,
+            @Param(id = 3) @Nullable InternalSetSchemaResponse internalSetSchemaResponse,
+            @Param(id = 4) @Nullable GetSchemaResponse getSchemaResponse,
+            @Param(id = 5) @Nullable List<String> strings,
+            @Param(id = 6) @Nullable GenericDocumentParcel genericDocumentParcel,
+            @Param(id = 7) @Nullable SearchResultPage searchResultPage,
+            @Param(id = 8) @Nullable List<MigrationFailure> migrationFailures,
+            @Param(id = 9) @Nullable List<SearchSuggestionResult> searchSuggestionResults,
+            @Param(id = 10) @Nullable StorageInfo storageInfo,
+            @Param(id = 11) @Nullable ExecuteAppFunctionResponse executeAppFunctionResponse) {
         mResultCode = resultCode;
-        mValue = value;
         mErrorMessage = errorMessage;
-        if (mResultCode == AppSearchResult.RESULT_OK) {
-            mResultCached = AppSearchResult.newSuccessfulResult((ValueType) mValue.getValue());
+        if (resultCode == AppSearchResult.RESULT_OK) {
+            mInternalSetSchemaResponse = internalSetSchemaResponse;
+            mGetSchemaResponse = getSchemaResponse;
+            mStrings = strings;
+            mGenericDocumentParcel = genericDocumentParcel;
+            mSearchResultPage = searchResultPage;
+            mMigrationFailures = migrationFailures;
+            mSearchSuggestionResults = searchSuggestionResults;
+            mStorageInfo = storageInfo;
+            mExecuteAppFunctionResponse = executeAppFunctionResponse;
+            if (mInternalSetSchemaResponse != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mInternalSetSchemaResponse);
+            } else if (mGetSchemaResponse != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mGetSchemaResponse);
+            } else if (mStrings != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>) AppSearchResult.newSuccessfulResult(mStrings);
+            } else if (mGenericDocumentParcel != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mGenericDocumentParcel);
+            } else if (mSearchResultPage != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mSearchResultPage);
+            } else if (mMigrationFailures != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mMigrationFailures);
+            } else if (mSearchSuggestionResults != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mSearchSuggestionResults);
+            } else if (mStorageInfo != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mStorageInfo);
+            } else if (mExecuteAppFunctionResponse != null) {
+                mResultCached =
+                        (AppSearchResult<ValueType>)
+                                AppSearchResult.newSuccessfulResult(mExecuteAppFunctionResponse);
+            } else {
+                // Default case where code is OK and value is null.
+                mResultCached = AppSearchResult.newSuccessfulResult(null);
+            }
         } else {
             mResultCached = AppSearchResult.newFailedResult(mResultCode, mErrorMessage);
         }
     }
 
-    /** Creates a new {@link AppSearchResultParcel} from the given result. */
-    public AppSearchResultParcel(@NonNull AppSearchResult<ValueType> result) {
-        mResultCached = result;
-        mResultCode = result.getResultCode();
-        if (mResultCode == AppSearchResult.RESULT_OK) {
-            mValue = new ValueParcel<>(result.getResultValue());
-            mErrorMessage = null;
-        } else {
-            mErrorMessage = result.getErrorMessage();
-            mValue = null;
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful Void
+     * response.
+     */
+    public static AppSearchResultParcel fromVoid() {
+        return new AppSearchResultParcel.Builder<>(AppSearchResult.RESULT_OK).build();
+    }
+
+    /** Creates a new failed {@link AppSearchResultParcel} from result code and error message. */
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    public static AppSearchResultParcel fromFailedResult(AppSearchResult failedResult) {
+        if (failedResult.isSuccess()) {
+            throw new IllegalStateException(
+                    "Creating a failed AppSearchResultParcel from a " + "successful response");
         }
+
+        return new AppSearchResultParcel.Builder<>(failedResult.getResultCode())
+                .setErrorMessage(failedResult.getErrorMessage())
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * InternalSetSchemaResponse}.
+     */
+    public static AppSearchResultParcel<InternalSetSchemaResponse> fromInternalSetSchemaResponse(
+            InternalSetSchemaResponse internalSetSchemaResponse) {
+        return new AppSearchResultParcel.Builder<InternalSetSchemaResponse>(
+                        AppSearchResult.RESULT_OK)
+                .setInternalSetSchemaResponse(internalSetSchemaResponse)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * GetSchemaResponse}.
+     */
+    public static AppSearchResultParcel<GetSchemaResponse> fromGetSchemaResponse(
+            GetSchemaResponse getSchemaResponse) {
+        return new AppSearchResultParcel.Builder<GetSchemaResponse>(AppSearchResult.RESULT_OK)
+                .setGetSchemaResponse(getSchemaResponse)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * List}&lt;{@link String}&gt;.
+     */
+    public static AppSearchResultParcel<List<String>> fromStringList(List<String> stringList) {
+        return new AppSearchResultParcel.Builder<List<String>>(AppSearchResult.RESULT_OK)
+                .setStrings(stringList)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * GenericDocumentParcel}.
+     */
+    public static AppSearchResultParcel<GenericDocumentParcel> fromGenericDocumentParcel(
+            GenericDocumentParcel genericDocumentParcel) {
+        return new AppSearchResultParcel.Builder<GenericDocumentParcel>(AppSearchResult.RESULT_OK)
+                .setGenericDocumentParcel(genericDocumentParcel)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * SearchResultPage}.
+     */
+    public static AppSearchResultParcel<SearchResultPage> fromSearchResultPage(
+            SearchResultPage searchResultPage) {
+        return new AppSearchResultParcel.Builder<SearchResultPage>(AppSearchResult.RESULT_OK)
+                .setSearchResultPage(searchResultPage)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * List}&lt;{@link MigrationFailure}&gt;.
+     */
+    public static AppSearchResultParcel<List<MigrationFailure>> fromMigrationFailuresList(
+            List<MigrationFailure> migrationFailureList) {
+        return new AppSearchResultParcel.Builder<List<MigrationFailure>>(AppSearchResult.RESULT_OK)
+                .setMigrationFailures(migrationFailureList)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * List}&lt;{@link SearchSuggestionResult}&gt;.
+     */
+    public static AppSearchResultParcel<List<SearchSuggestionResult>>
+            fromSearchSuggestionResultList(
+                    List<SearchSuggestionResult> searchSuggestionResultList) {
+        return new AppSearchResultParcel.Builder<List<SearchSuggestionResult>>(
+                        AppSearchResult.RESULT_OK)
+                .setSearchSuggestionResults(searchSuggestionResultList)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * StorageInfo}.
+     */
+    public static AppSearchResultParcel<StorageInfo> fromStorageInfo(StorageInfo storageInfo) {
+        return new AppSearchResultParcel.Builder<StorageInfo>(AppSearchResult.RESULT_OK)
+                .setStorageInfo(storageInfo)
+                .build();
+    }
+
+    /**
+     * Creates a new {@link AppSearchResultParcel} from the given result in case a successful {@link
+     * ExecuteAppFunctionResponse}.
+     */
+    public static AppSearchResultParcel<ExecuteAppFunctionResponse> fromExecuteAppFunctionResponse(
+            ExecuteAppFunctionResponse executeAppFunctionResponse) {
+        return new AppSearchResultParcel.Builder<ExecuteAppFunctionResponse>(
+                        AppSearchResult.RESULT_OK)
+                .setExecuteAppFunctionResponse(executeAppFunctionResponse)
+                .build();
     }
 
     @NonNull
@@ -124,12 +353,120 @@
         ParcelableUtil.writeBlob(dest, bytes);
     }
 
-    static void directlyWriteToParcel(@NonNull AppSearchResultParcel result, @NonNull Parcel data,
-            int flags) {
+    static void directlyWriteToParcel(
+            @NonNull AppSearchResultParcel<?> result, @NonNull Parcel data, int flags) {
         AppSearchResultParcelCreator.writeToParcel(result, data, flags);
     }
 
-    static AppSearchResultParcel directlyReadFromParcel(@NonNull Parcel data) {
+    static AppSearchResultParcel<?> directlyReadFromParcel(@NonNull Parcel data) {
         return CREATOR_WITHOUT_BLOB.createFromParcel(data);
     }
+
+    /**
+     * Builder for {@link AppSearchResultParcel} objects.
+     *
+     * @param <ValueType> The type of the result objects for successful results.
+     */
+    static final class Builder<ValueType> {
+
+        @AppSearchResult.ResultCode private final int mResultCode;
+        @Nullable private String mErrorMessage;
+        @Nullable private InternalSetSchemaResponse mInternalSetSchemaResponse;
+        @Nullable private GetSchemaResponse mGetSchemaResponse;
+        @Nullable private List<String> mStrings;
+        @Nullable private GenericDocumentParcel mGenericDocumentParcel;
+        @Nullable private SearchResultPage mSearchResultPage;
+        @Nullable private List<MigrationFailure> mMigrationFailures;
+        @Nullable private List<SearchSuggestionResult> mSearchSuggestionResults;
+        @Nullable private StorageInfo mStorageInfo;
+        @Nullable private ExecuteAppFunctionResponse mExecuteAppFunctionResponse;
+
+        /** Builds an {@link AppSearchResultParcel.Builder}. */
+        Builder(int resultCode) {
+            mResultCode = resultCode;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setErrorMessage(@Nullable String errorMessage) {
+            mErrorMessage = errorMessage;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setInternalSetSchemaResponse(
+                InternalSetSchemaResponse internalSetSchemaResponse) {
+            mInternalSetSchemaResponse = internalSetSchemaResponse;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setGetSchemaResponse(GetSchemaResponse getSchemaResponse) {
+            mGetSchemaResponse = getSchemaResponse;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setStrings(List<String> strings) {
+            mStrings = strings;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setGenericDocumentParcel(GenericDocumentParcel genericDocumentParcel) {
+            mGenericDocumentParcel = genericDocumentParcel;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setSearchResultPage(SearchResultPage searchResultPage) {
+            mSearchResultPage = searchResultPage;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setMigrationFailures(List<MigrationFailure> migrationFailures) {
+            mMigrationFailures = migrationFailures;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setSearchSuggestionResults(
+                List<SearchSuggestionResult> searchSuggestionResults) {
+            mSearchSuggestionResults = searchSuggestionResults;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setStorageInfo(StorageInfo storageInfo) {
+            mStorageInfo = storageInfo;
+            return this;
+        }
+
+        @CanIgnoreReturnValue
+        Builder<ValueType> setExecuteAppFunctionResponse(
+                ExecuteAppFunctionResponse executeAppFunctionResponse) {
+            mExecuteAppFunctionResponse = executeAppFunctionResponse;
+            return this;
+        }
+
+        /**
+         * Builds an {@link AppSearchResultParcel} object from the contents of this {@link
+         * AppSearchResultParcel.Builder}.
+         */
+        @NonNull
+        AppSearchResultParcel<ValueType> build() {
+            return new AppSearchResultParcel<>(
+                    mResultCode,
+                    mErrorMessage,
+                    mInternalSetSchemaResponse,
+                    mGetSchemaResponse,
+                    mStrings,
+                    mGenericDocumentParcel,
+                    mSearchResultPage,
+                    mMigrationFailures,
+                    mSearchSuggestionResults,
+                    mStorageInfo,
+                    mExecuteAppFunctionResponse);
+        }
+    }
 }
diff --git a/framework/java/android/app/appsearch/aidl/DocumentsParcel.java b/framework/java/android/app/appsearch/aidl/DocumentsParcel.java
index 9bbc031..297f8c4 100644
--- a/framework/java/android/app/appsearch/aidl/DocumentsParcel.java
+++ b/framework/java/android/app/appsearch/aidl/DocumentsParcel.java
@@ -24,14 +24,14 @@
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
-import java.util.ArrayList;
+
 import java.util.List;
 import java.util.Objects;
 
 /**
  * The Parcelable object contains a List of {@link GenericDocument}.
  *
- * <P>This class will batch a list of {@link GenericDocument}. If the number of documents is too
+ * <p>This class will batch a list of {@link GenericDocument}. If the number of documents is too
  * large for a transaction, they will be put to Android Shared Memory.
  *
  * @see Parcel#writeBlob(byte[])
@@ -39,26 +39,29 @@
  */
 @SafeParcelable.Class(creator = "DocumentsParcelCreator", creatorIsFinal = false)
 public final class DocumentsParcel extends AbstractSafeParcelable {
-    public static final DocumentsParcelCreator CREATOR = new DocumentsParcelCreator() {
-        @Override
-        public DocumentsParcel createFromParcel(Parcel in) {
-            byte[] dataBlob = ParcelableUtil.readBlob(in);
-            // Create a parcel object to un-serialize the byte array we are reading from
-            // Parcel.readBlob(). Parcel.WriteBlob() could take care of whether to pass data via
-            // binder directly or Android shared memory if the data is large.
-            Parcel unmarshallParcel = Parcel.obtain();
-            try {
-                unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
-                unmarshallParcel.setDataPosition(0);
-                return super.createFromParcel(unmarshallParcel);
-            } finally {
-                unmarshallParcel.recycle();
-            }
-        }
-    };
+    public static final Parcelable.Creator<DocumentsParcel> CREATOR =
+            new DocumentsParcelCreator() {
+                @Override
+                public DocumentsParcel createFromParcel(Parcel in) {
+                    byte[] dataBlob = Objects.requireNonNull(ParcelableUtil.readBlob(in));
+                    // Create a parcel object to un-serialize the byte array we are reading from
+                    // Parcel.readBlob(). Parcel.WriteBlob() could take care of whether to pass data
+                    // via
+                    // binder directly or Android shared memory if the data is large.
+                    Parcel unmarshallParcel = Parcel.obtain();
+                    try {
+                        unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length);
+                        unmarshallParcel.setDataPosition(0);
+                        return super.createFromParcel(unmarshallParcel);
+                    } finally {
+                        unmarshallParcel.recycle();
+                    }
+                }
+            };
 
     @Field(id = 1, getter = "getDocumentParcels")
     final List<GenericDocumentParcel> mDocumentParcels;
+
     @Field(id = 2, getter = "getTakenActionGenericDocumentParcels")
     final List<GenericDocumentParcel> mTakenActionGenericDocumentParcels;
 
@@ -95,19 +98,19 @@
         return bytes;
     }
 
-    /**  Returns the List of {@link GenericDocument} of this object. */
+    /** Returns the List of {@link GenericDocument} of this object. */
     @NonNull
     public List<GenericDocumentParcel> getDocumentParcels() {
         return mDocumentParcels;
     }
 
-    /**  Returns the List of TakenActions as {@link GenericDocument}. */
+    /** Returns the List of TakenActions as {@link GenericDocument}. */
     @NonNull
     public List<GenericDocumentParcel> getTakenActionGenericDocumentParcels() {
-       return mTakenActionGenericDocumentParcels;
+        return mTakenActionGenericDocumentParcels;
     }
 
-    /**  Returns sum of the counts of Documents and TakenActionGenericDocuments. */
+    /** Returns sum of the counts of Documents and TakenActionGenericDocuments. */
     public int getTotalDocumentCount() {
         return mDocumentParcels.size() + mTakenActionGenericDocumentParcels.size();
     }
diff --git a/framework/java/android/app/appsearch/aidl/ExecuteAppFunctionAidlRequest.aidl b/framework/java/android/app/appsearch/aidl/ExecuteAppFunctionAidlRequest.aidl
new file mode 100644
index 0000000..2cb21b3
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/ExecuteAppFunctionAidlRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2024, 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.aidl;
+
+/** {@hide} */
+parcelable ExecuteAppFunctionAidlRequest;
diff --git a/framework/java/android/app/appsearch/aidl/ExecuteAppFunctionAidlRequest.java b/framework/java/android/app/appsearch/aidl/ExecuteAppFunctionAidlRequest.java
new file mode 100644
index 0000000..9008633
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/ExecuteAppFunctionAidlRequest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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.aidl;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.app.appsearch.functions.ExecuteAppFunctionRequest;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates a request to make a binder call to execute an app function.
+ *
+ * @hide
+ */
[email protected](creator = "ExecuteAppFunctionAidlRequestCreator")
+public final class ExecuteAppFunctionAidlRequest extends AbstractSafeParcelable {
+    @NonNull
+    public static final Parcelable.Creator<ExecuteAppFunctionAidlRequest> CREATOR =
+            new ExecuteAppFunctionAidlRequestCreator();
+
+    @NonNull
+    @Field(id = 1, getter = "getClientRequest")
+    private final ExecuteAppFunctionRequest mClientRequest;
+
+    @NonNull
+    @Field(id = 2, getter = "getCallerAttributionSource")
+    private final AppSearchAttributionSource mCallerAttributionSource;
+
+    @Field(id = 3, getter = "getUserHandle")
+    private final UserHandle mUserHandle;
+
+    @Field(id = 4, getter = "getBinderCallStartTimeMillis")
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
+    @Constructor
+    public ExecuteAppFunctionAidlRequest(
+            @Param(id = 1) @NonNull ExecuteAppFunctionRequest clientRequest,
+            @Param(id = 2) @NonNull AppSearchAttributionSource callerAttributionSource,
+            @Param(id = 3) @NonNull UserHandle userHandle,
+            @Param(id = 4) long binderCallStartTimeMillis) {
+        mClientRequest = Objects.requireNonNull(clientRequest);
+        mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
+        mUserHandle = Objects.requireNonNull(userHandle);
+        mBinderCallStartTimeMillis = binderCallStartTimeMillis;
+    }
+
+    /** Returns the original request created by the client. */
+    @NonNull
+    public ExecuteAppFunctionRequest getClientRequest() {
+        return mClientRequest;
+    }
+
+    @NonNull
+    public AppSearchAttributionSource getCallerAttributionSource() {
+        return mCallerAttributionSource;
+    }
+
+    @NonNull
+    public UserHandle getUserHandle() {
+        return mUserHandle;
+    }
+
+    @ElapsedRealtimeLong
+    public long getBinderCallStartTimeMillis() {
+        return mBinderCallStartTimeMillis;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ExecuteAppFunctionAidlRequestCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/framework/java/android/app/appsearch/aidl/GetDocumentsAidlRequest.aidl b/framework/java/android/app/appsearch/aidl/GetDocumentsAidlRequest.aidl
new file mode 100644
index 0000000..a3de17b
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/GetDocumentsAidlRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2024, 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.aidl;
+
+/** {@hide} */
+parcelable GetDocumentsAidlRequest;
diff --git a/framework/java/android/app/appsearch/aidl/GetDocumentsAidlRequest.java b/framework/java/android/app/appsearch/aidl/GetDocumentsAidlRequest.java
new file mode 100644
index 0000000..f29354b
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/GetDocumentsAidlRequest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 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.aidl;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.app.appsearch.GetByDocumentIdRequest;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates a request to make a binder call to retrieve documents from the index.
+ *
+ * @hide
+ */
[email protected](creator = "GetDocumentsAidlRequestCreator")
+public class GetDocumentsAidlRequest extends AbstractSafeParcelable {
+    @NonNull
+    public static final Parcelable.Creator<GetDocumentsAidlRequest> CREATOR =
+            new GetDocumentsAidlRequestCreator();
+
+    // The permission identity of the package that is getting this document.
+    @NonNull
+    @Field(id = 1, getter = "getCallerAttributionSource")
+    private final AppSearchAttributionSource mCallerAttributionSource;
+
+    // The name of the package that owns this document.
+    @NonNull
+    @Field(id = 2, getter = "getTargetPackageName")
+    private final String mTargetPackageName;
+
+    // The name of the package that owns this document.
+    @NonNull
+    @Field(id = 3, getter = "getDatabaseName")
+    private final String mDatabaseName;
+
+    // The request to retrieve by namespace and IDs from the {@link
+    // AppSearchSession} database for this document.
+    @NonNull
+    @Field(id = 4, getter = "getGetByDocumentIdRequest")
+    private final GetByDocumentIdRequest mGetByDocumentIdRequest;
+
+    // The Handle of the calling user.
+    @NonNull
+    @Field(id = 5, getter = "getUserHandle")
+    private final UserHandle mUserHandle;
+
+    // The start timestamp of binder call in Millis.
+    @Field(id = 6, getter = "getBinderCallStartTimeMillis")
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
+    // Whether to query the user's enterprise profile AppSearch instance
+    @Field(id = 7, getter = "isForEnterprise")
+    private final boolean mIsForEnterprise;
+
+    /**
+     * Retrieves documents from the index.
+     *
+     * @param callerAttributionSource The permission identity of the package that is getting this
+     *     document.
+     * @param targetPackageName The name of the package that owns this document.
+     * @param databaseName The databaseName this document resides in.
+     * @param getByDocumentIdRequest The {@link GetByDocumentIdRequest} to retrieve document.
+     * @param userHandle Handle of the calling user.
+     * @param binderCallStartTimeMillis start timestamp of binder call in Millis.
+     * @param isForEnterprise Whether to query the user's enterprise profile AppSearch instance
+     */
+    @Constructor
+    public GetDocumentsAidlRequest(
+            @Param(id = 1) @NonNull AppSearchAttributionSource callerAttributionSource,
+            @Param(id = 2) @NonNull String targetPackageName,
+            @Param(id = 3) @NonNull String databaseName,
+            @Param(id = 4) @NonNull GetByDocumentIdRequest getByDocumentIdRequest,
+            @Param(id = 5) @NonNull UserHandle userHandle,
+            @Param(id = 6) long binderCallStartTimeMillis,
+            @Param(id = 7) boolean isForEnterprise) {
+        mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
+        mTargetPackageName = Objects.requireNonNull(targetPackageName);
+        mDatabaseName = Objects.requireNonNull(databaseName);
+        mGetByDocumentIdRequest = Objects.requireNonNull(getByDocumentIdRequest);
+        mUserHandle = Objects.requireNonNull(userHandle);
+        mBinderCallStartTimeMillis = binderCallStartTimeMillis;
+        mIsForEnterprise = isForEnterprise;
+    }
+
+    @NonNull
+    public AppSearchAttributionSource getCallerAttributionSource() {
+        return mCallerAttributionSource;
+    }
+
+    @NonNull
+    public String getTargetPackageName() {
+        return mTargetPackageName;
+    }
+
+    @NonNull
+    public String getDatabaseName() {
+        return mDatabaseName;
+    }
+
+    @NonNull
+    public GetByDocumentIdRequest getGetByDocumentIdRequest() {
+        return mGetByDocumentIdRequest;
+    }
+
+    @NonNull
+    public UserHandle getUserHandle() {
+        return mUserHandle;
+    }
+
+    @ElapsedRealtimeLong
+    public long getBinderCallStartTimeMillis() {
+        return mBinderCallStartTimeMillis;
+    }
+
+    public boolean isForEnterprise() {
+        return mIsForEnterprise;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        GetDocumentsAidlRequestCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/framework/java/android/app/appsearch/aidl/GetNamespacesAidlRequest.java b/framework/java/android/app/appsearch/aidl/GetNamespacesAidlRequest.java
index 8f6b3df..4ab9fc6 100644
--- a/framework/java/android/app/appsearch/aidl/GetNamespacesAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/GetNamespacesAidlRequest.java
@@ -20,29 +20,34 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to retrieve all namespaces in given database.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "GetNamespacesAidlRequestCreator")
 public class GetNamespacesAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final GetNamespacesAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<GetNamespacesAidlRequest> CREATOR =
             new GetNamespacesAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 4, getter = "getBinderCallStartTimeMillis")
     private final long mBinderCallStartTimeMillis;
 
@@ -50,7 +55,7 @@
      * Retrieves the set of all namespaces in the current database with at least one document.
      *
      * @param callerAttributionSource The permission identity of the package that owns the schema.
-     * @param databaseName  The name of the database to retrieve.
+     * @param databaseName The name of the database to retrieve.
      * @param userHandle Handle of the calling user
      * @param binderCallStartTimeMillis start timestamp of binder call in Millis
      */
diff --git a/framework/java/android/app/appsearch/aidl/GetNextPageAidlRequest.java b/framework/java/android/app/appsearch/aidl/GetNextPageAidlRequest.java
index 500b5e9..034d7fd 100644
--- a/framework/java/android/app/appsearch/aidl/GetNextPageAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/GetNextPageAidlRequest.java
@@ -23,6 +23,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -30,28 +31,38 @@
 /**
  * Encapsulates a request to make a binder call to fetch the next page of results of a previously
  * executed search.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "GetNextPageAidlRequestCreator")
 public class GetNextPageAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final GetNextPageAidlRequestCreator CREATOR = new GetNextPageAidlRequestCreator();
+    public static final Parcelable.Creator<GetNextPageAidlRequest> CREATOR =
+            new GetNextPageAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @Nullable
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @Field(id = 3, getter = "getNextPageToken")
     private final long mNextPageToken;
+
     @Field(id = 4, getter = "getJoinType")
-    private final @AppSearchSchema.StringPropertyConfig.JoinableValueType int mJoinType;
+    @AppSearchSchema.StringPropertyConfig.JoinableValueType
+    private final int mJoinType;
+
     @NonNull
     @Field(id = 5, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 6, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
     @Field(id = 7, getter = "isForEnterprise")
     private final boolean mIsForEnterprise;
 
@@ -59,10 +70,9 @@
      * Fetches the next page of results of a previously executed search. Results can be empty if
      * next-page token is invalid or all pages have been returned.
      *
-     * @param callerAttributionSource The permission identity of the package to persist to disk
-     *     for.
+     * @param callerAttributionSource The permission identity of the package to persist to disk for.
      * @param databaseName The nullable databaseName this search for. The databaseName will be null
-     *                     if the search is a global search.
+     *     if the search is a global search.
      * @param nextPageToken The token of pre-loaded results of previously executed search.
      * @param joinType the type of join performed. 0 if no join is performed
      * @param userHandle Handle of the calling user
diff --git a/framework/java/android/app/appsearch/aidl/GetSchemaAidlRequest.java b/framework/java/android/app/appsearch/aidl/GetSchemaAidlRequest.java
index 84ff0b1..98cf304 100644
--- a/framework/java/android/app/appsearch/aidl/GetSchemaAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/GetSchemaAidlRequest.java
@@ -21,34 +21,42 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to get the schema for a given database.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "GetSchemaAidlRequestCreator")
 public class GetSchemaAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final GetSchemaAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<GetSchemaAidlRequest> CREATOR =
             new GetSchemaAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getTargetPackageName")
     private final String mTargetPackageName;
+
     @NonNull
     @Field(id = 3, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 4, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 5, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
     @Field(id = 6, getter = "isForEnterprise")
     private final boolean mIsForEnterprise;
 
@@ -57,7 +65,7 @@
      *
      * @param callerAttributionSource The permission identity of the package making this call.
      * @param targetPackageName The name of the package that owns the schema.
-     * @param databaseName  The name of the database to retrieve.
+     * @param databaseName The name of the database to retrieve.
      * @param userHandle Handle of the calling user
      * @param binderCallStartTimeMillis start timestamp of binder call in Millis
      */
diff --git a/framework/java/android/app/appsearch/aidl/GetStorageInfoAidlRequest.java b/framework/java/android/app/appsearch/aidl/GetStorageInfoAidlRequest.java
index 53d8c4f..08e7200 100644
--- a/framework/java/android/app/appsearch/aidl/GetStorageInfoAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/GetStorageInfoAidlRequest.java
@@ -21,37 +21,43 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to get the storage info.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "GetStorageInfoAidlRequestCreator")
 public class GetStorageInfoAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final GetStorageInfoAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<GetStorageInfoAidlRequest> CREATOR =
             new GetStorageInfoAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 4, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Gets the storage info.
      *
-     * @param callerAttributionSource The permission identity of the package to get the storage
-     *     info for.
+     * @param callerAttributionSource The permission identity of the package to get the storage info
+     *     for.
      * @param databaseName The databaseName to get the storage info for.
      * @param userHandle Handle of the calling user
      * @param binderCallStartTimeMillis start timestamp of binder call in Millis
diff --git a/framework/java/android/app/appsearch/aidl/GlobalSearchAidlRequest.java b/framework/java/android/app/appsearch/aidl/GlobalSearchAidlRequest.java
index b318eda..d286170 100644
--- a/framework/java/android/app/appsearch/aidl/GlobalSearchAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/GlobalSearchAidlRequest.java
@@ -22,6 +22,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -29,28 +30,35 @@
 /**
  * Encapsulates a request to make a binder call to search over all permitted databases in the
  * AppSearch index.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "GlobalSearchAidlRequestCreator")
 public class GlobalSearchAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final GlobalSearchAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<GlobalSearchAidlRequest> CREATOR =
             new GlobalSearchAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getSearchExpression")
     private final String mSearchExpression;
+
     @NonNull
     @Field(id = 3, getter = "getSearchSpec")
     private final SearchSpec mSearchSpec;
+
     @NonNull
     @Field(id = 4, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 5, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
     @Field(id = 6, getter = "isForEnterprise")
     private final boolean mIsForEnterprise;
 
diff --git a/framework/java/android/app/appsearch/aidl/IAppFunctionService.aidl b/framework/java/android/app/appsearch/aidl/IAppFunctionService.aidl
new file mode 100644
index 0000000..9c0e14a
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/IAppFunctionService.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.aidl;
+
+import android.os.Bundle;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.app.appsearch.functions.ExecuteAppFunctionRequest;
+
+
+ /** {@hide} */
+oneway interface IAppFunctionService {
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * @param request  the function execution request.
+     * @param callback a callback to report back the result.
+     */
+    void executeAppFunction(
+        in ExecuteAppFunctionRequest request,
+        in IAppSearchResultCallback callback
+    );
+}
\ No newline at end of file
diff --git a/framework/java/android/app/appsearch/aidl/IAppSearchManager.aidl b/framework/java/android/app/appsearch/aidl/IAppSearchManager.aidl
index 3b4be4e..b1d0ab0 100644
--- a/framework/java/android/app/appsearch/aidl/IAppSearchManager.aidl
+++ b/framework/java/android/app/appsearch/aidl/IAppSearchManager.aidl
@@ -23,7 +23,9 @@
 import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
 import android.app.appsearch.aidl.IAppSearchObserverProxy;
 import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.app.appsearch.aidl.ExecuteAppFunctionAidlRequest;
 import android.app.appsearch.aidl.DocumentsParcel;
+import android.app.appsearch.aidl.GetDocumentsAidlRequest;
 import android.app.appsearch.aidl.GetNamespacesAidlRequest;
 import android.app.appsearch.aidl.GetNextPageAidlRequest;
 import android.app.appsearch.aidl.GetSchemaAidlRequest;
@@ -37,6 +39,7 @@
 import android.app.appsearch.aidl.RegisterObserverCallbackAidlRequest;
 import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
 import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
+import android.app.appsearch.aidl.ReportUsageAidlRequest;
 import android.app.appsearch.aidl.SearchAidlRequest;
 import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
 import android.app.appsearch.aidl.SetSchemaAidlRequest;
@@ -120,17 +123,8 @@
     /**
      * Retrieves documents from the index.
      *
-     * @param callerAttributionSource The permission identity of the package that is getting this
-     *     document.
-     * @param targetPackageName The name of the package that owns this document.
-     * @param databaseName  The databaseName this document resides in.
-     * @param namespace    The namespace this document resides in.
-     * @param ids The IDs of the documents to retrieve.
-     * @param typePropertyPaths A map of schema type to a list of property paths to return in the
-     *     result.
-     * @param userHandle Handle of the calling user.
-     * @param binderCallStartTimeMillis start timestamp of binder call in Millis.
-     * @param isForEnterprise Whether to query the user's enterprise profile AppSearch instance
+     * @param request {@link GetDocumentsAidlRequest} that contains the input parameters for the
+     *     get documents operation.
      * @param callback
      *     If the call fails to start, {@link IAppSearchBatchResultCallback#onSystemError}
      *     will be called with the cause throwable. Otherwise,
@@ -139,15 +133,7 @@
      *     where the keys are document IDs, and the values are Document bundles.
      */
     void getDocuments(
-        in AppSearchAttributionSource callerAttributionSource,
-        in String targetPackageName,
-        in String databaseName,
-        in String namespace,
-        in List<String> ids,
-        in Map<String, List<String>> typePropertyPaths,
-        in UserHandle userHandle,
-        in long binderCallStartTimeMillis,
-        in boolean isForEnterprise,
+        in GetDocumentsAidlRequest request,
         in IAppSearchBatchResultCallback callback) = 5;
 
     /**
@@ -252,29 +238,13 @@
      *
      * <p>Reporting usage of a document is optional.
      *
-     * @param callerAttributionSource The permission identity of the package that owns this
-     *     document.
-     * @param targetPackageName The name of the package that owns this document.
-     * @param databaseName  The name of the database to report usage against.
-     * @param namespace Namespace the document being used belongs to.
-     * @param id ID of the document being used.
-     * @param usageTimestampMillis The timestamp at which the document was used.
-     * @param systemUsage Whether the usage was reported by a system app against another app's doc.
-     * @param userHandle Handle of the calling user
-     * @param binderCallStartTimeMillis start timestamp of binder call in Millis
+     * @param request {@link ReportUsageAidlRequest} contains the input parameters for report
+     *     usage operation.
      * @param callback {@link IAppSearchResultCallback#onResult} will be called with an
      *     {@link AppSearchResult}&lt;{@link Void}&gt;.
      */
     void reportUsage(
-        in AppSearchAttributionSource callerAttributionSource,
-        in String targetPackageName,
-        in String databaseName,
-        in String namespace,
-        in String id,
-        in long usageTimestampMillis,
-        in boolean systemUsage,
-        in UserHandle userHandle,
-        in long binderCallStartTimeMillis,
+        in ReportUsageAidlRequest request,
         in IAppSearchResultCallback callback) = 13;
 
     /**
@@ -352,5 +322,15 @@
         in UnregisterObserverCallbackAidlRequest request,
         in IAppSearchObserverProxy observerProxy) = 19;
 
-    // next function transaction ID = 20;
+    /**
+     * Executes an app function provided by {@link AppFunctionService} through the system.
+     *
+     * @param request the request to execute an app function.
+     * @param callback the callback to report the result.
+     */
+   void executeAppFunction(
+       in ExecuteAppFunctionAidlRequest request,
+       in IAppSearchResultCallback callback) = 20;
+
+    // next function transaction ID = 21;
 }
diff --git a/framework/java/android/app/appsearch/aidl/InitializeAidlRequest.java b/framework/java/android/app/appsearch/aidl/InitializeAidlRequest.java
index 3aacdb0..e230723 100644
--- a/framework/java/android/app/appsearch/aidl/InitializeAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/InitializeAidlRequest.java
@@ -21,6 +21,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -28,22 +29,26 @@
 /**
  * Encapsulates a request to make a binder call to create and initialize AppSearchImpl for the
  * calling application.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "InitializeAidlRequestCreator")
 public class InitializeAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final InitializeAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<InitializeAidlRequest> CREATOR =
             new InitializeAidlRequestCreator();
 
     @NonNull
     @SafeParcelable.Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 3, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Creates and initializes AppSearchImpl for the calling app.
diff --git a/framework/java/android/app/appsearch/aidl/InvalidateNextPageTokenAidlRequest.java b/framework/java/android/app/appsearch/aidl/InvalidateNextPageTokenAidlRequest.java
index 147d2d9..927de9d 100644
--- a/framework/java/android/app/appsearch/aidl/InvalidateNextPageTokenAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/InvalidateNextPageTokenAidlRequest.java
@@ -21,6 +21,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -28,24 +29,30 @@
 /**
  * Encapsulates a request to make a binder call to invalidate the next-page token so that no more
  * results of the related search can be returned.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "InvalidateNextPageTokenAidlRequestCreator")
 public class InvalidateNextPageTokenAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final InvalidateNextPageTokenAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<InvalidateNextPageTokenAidlRequest> CREATOR =
             new InvalidateNextPageTokenAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @Field(id = 2, getter = "getNextPageToken")
     private final long mNextPageToken;
+
     @NonNull
     @Field(id = 3, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 4, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
     @Field(id = 5, getter = "isForEnterprise")
     private final boolean mIsForEnterprise;
 
@@ -53,10 +60,9 @@
      * Invalidates the next-page token so that no more results of the related search can be
      * returned.
      *
-     * @param callerAttributionSource The permission identity of the package to persist to disk
-     *     for.
+     * @param callerAttributionSource The permission identity of the package to persist to disk for.
      * @param nextPageToken The token of pre-loaded results of previously executed search to be
-     *                      invalidated.
+     *     invalidated.
      * @param userHandle Handle of the calling user
      * @param binderCallStartTimeMillis start timestamp of binder call in Millis
      * @param isForEnterprise Whether to user the user's enterprise profile AppSearch instance
diff --git a/framework/java/android/app/appsearch/aidl/PersistToDiskAidlRequest.java b/framework/java/android/app/appsearch/aidl/PersistToDiskAidlRequest.java
index a7d8671..d63d6a2 100644
--- a/framework/java/android/app/appsearch/aidl/PersistToDiskAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/PersistToDiskAidlRequest.java
@@ -16,31 +16,36 @@
 
 package android.app.appsearch.aidl;
 
+import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
-import android.app.appsearch.AppSearchSession;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to persist all update/delete requests to the disk.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "PersistToDiskAidlRequestCreator")
 public class PersistToDiskAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final PersistToDiskAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<PersistToDiskAidlRequest> CREATOR =
             new PersistToDiskAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
+    @ElapsedRealtimeLong
     @Field(id = 3, getter = "getBinderCallStartTimeMillis")
     private final long mBinderCallStartTimeMillis;
 
@@ -55,7 +60,7 @@
     public PersistToDiskAidlRequest(
             @Param(id = 1) @NonNull AppSearchAttributionSource callerAttributionSource,
             @Param(id = 2) @NonNull UserHandle userHandle,
-            @Param(id = 3) @NonNull long binderCallStartTimeMillis) {
+            @Param(id = 3) @ElapsedRealtimeLong long binderCallStartTimeMillis) {
         mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
         mUserHandle = Objects.requireNonNull(userHandle);
         mBinderCallStartTimeMillis = binderCallStartTimeMillis;
@@ -71,6 +76,7 @@
         return mUserHandle;
     }
 
+    @ElapsedRealtimeLong
     public long getBinderCallStartTimeMillis() {
         return mBinderCallStartTimeMillis;
     }
diff --git a/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.aidl b/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.aidl
index 867eabd..7bc79d8 100644
--- a/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.aidl
+++ b/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.aidl
@@ -16,4 +16,4 @@
 package android.app.appsearch.aidl;
 
 /** {@hide} */
-parcelable PutDocumentsAidlRequest;
\ No newline at end of file
+parcelable PutDocumentsAidlRequest;
diff --git a/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.java b/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.java
index 4c2c3f3..ff6e132 100644
--- a/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/PutDocumentsAidlRequest.java
@@ -18,39 +18,55 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
-import android.app.appsearch.aidl.DocumentsParcel;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to insert documents into the index.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "PutDocumentsAidlRequestCreator")
 public class PutDocumentsAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final PutDocumentsAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<PutDocumentsAidlRequest> CREATOR =
             new PutDocumentsAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getDocumentsParcel")
     private final DocumentsParcel mDocumentsParcel;
+
     @NonNull
     @Field(id = 4, getter = "getUserHandle")
     private final UserHandle mUserHandle;
-    @Field(id = 5, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
 
+    @Field(id = 5, getter = "getBinderCallStartTimeMillis")
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
+    /**
+     * Inserts documents into the index.
+     *
+     * @param callerAttributionSource The permission identity of the package that owns this
+     *     document.
+     * @param databaseName The name of the database where this document lives.
+     * @param documentsParcel The Parcelable object contains a list of GenericDocument.
+     * @param userHandle The Handle of the calling user.
+     * @param binderCallStartTimeMillis The start timestamp of binder call in Millis.
+     */
     @Constructor
     public PutDocumentsAidlRequest(
             @Param(id = 1) @NonNull AppSearchAttributionSource callerAttributionSource,
@@ -94,4 +110,4 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         PutDocumentsAidlRequestCreator.writeToParcel(this, dest, flags);
     }
-}
\ No newline at end of file
+}
diff --git a/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.aidl b/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.aidl
index 92131b7..3461b68 100644
--- a/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.aidl
+++ b/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.aidl
@@ -16,4 +16,4 @@
 package android.app.appsearch.aidl;
 
 /** {@hide} */
-parcelable PutDocumentsFromFileAidlRequest;
\ No newline at end of file
+parcelable PutDocumentsFromFileAidlRequest;
diff --git a/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.java b/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.java
index b3b4b5b..3471678 100644
--- a/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/PutDocumentsFromFileAidlRequest.java
@@ -23,6 +23,7 @@
 import android.app.appsearch.stats.SchemaMigrationStats;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -30,33 +31,42 @@
 /**
  * Encapsulates a request to make a binder call to insert documents from the given file into the
  * index.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "PutDocumentsFromFileAidlRequestCreator")
 public class PutDocumentsFromFileAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final PutDocumentsFromFileAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<PutDocumentsFromFileAidlRequest> CREATOR =
             new PutDocumentsFromFileAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getParcelFileDescriptor")
     private final ParcelFileDescriptor mParcelFileDescriptor;
+
     @NonNull
     @Field(id = 4, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @NonNull
     @Field(id = 5, getter = "getSchemaMigrationStats")
     private final SchemaMigrationStats mSchemaMigrationStats;
+
     @Field(id = 6, getter = "getTotalLatencyStartTimeMillis")
-    private final @ElapsedRealtimeLong long mTotalLatencyStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mTotalLatencyStartTimeMillis;
+
     @Field(id = 7, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Inserts documents from the given file into the index.
@@ -67,7 +77,7 @@
      *
      * @param callerAttributionSource The permission identity of the package that owns this
      *     document.
-     * @param databaseName  The name of the database where this document lives.
+     * @param databaseName The name of the database where this document lives.
      * @param parcelFileDescriptor The ParcelFileDescriptor where documents should be read from.
      * @param userHandle Handle of the calling user.
      * @param schemaMigrationStats the Parcelable contains SchemaMigrationStats information.
diff --git a/framework/java/android/app/appsearch/aidl/RegisterObserverCallbackAidlRequest.java b/framework/java/android/app/appsearch/aidl/RegisterObserverCallbackAidlRequest.java
index 495c530..5e61ad5 100644
--- a/framework/java/android/app/appsearch/aidl/RegisterObserverCallbackAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/RegisterObserverCallbackAidlRequest.java
@@ -22,40 +22,47 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to add an observer monitor changes in the database.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "RegisterObserverCallbackAidlRequestCreator")
 public class RegisterObserverCallbackAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final RegisterObserverCallbackAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<RegisterObserverCallbackAidlRequest> CREATOR =
             new RegisterObserverCallbackAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getTargetPackageName")
     private final String mTargetPackageName;
+
     @NonNull
     @Field(id = 3, getter = "getObserverSpec")
     private final ObserverSpec mObserverSpec;
+
     @NonNull
     @Field(id = 4, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 5, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Creates and initializes AppSearchImpl for the calling app.
      *
-     * @param callerAttributionSource The permission identity of the package which is registering
-     *     an observer.
+     * @param callerAttributionSource The permission identity of the package which is registering an
+     *     observer.
      * @param targetPackageName Package whose changes to monitor
      * @param observerSpec ObserverSpec showing what types of changes to listen for
      * @param userHandle Handle of the calling user
diff --git a/framework/java/android/app/appsearch/aidl/RemoveByDocumentIdAidlRequest.java b/framework/java/android/app/appsearch/aidl/RemoveByDocumentIdAidlRequest.java
index f74608d..8a42dfe 100644
--- a/framework/java/android/app/appsearch/aidl/RemoveByDocumentIdAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/RemoveByDocumentIdAidlRequest.java
@@ -18,49 +18,52 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
+import android.app.appsearch.RemoveByDocumentIdRequest;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
-import java.util.List;
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to remove documents by id.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "RemoveByDocumentIdAidlRequestCreator")
 public class RemoveByDocumentIdAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final RemoveByDocumentIdAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<RemoveByDocumentIdAidlRequest> CREATOR =
             new RemoveByDocumentIdAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
-    @Field(id = 3, getter = "getNamespace")
-    private final String mNamespace;
+    @Field(id = 3, getter = "getRemoveByDocumentIdRequest")
+    final RemoveByDocumentIdRequest mRemoveByDocumentIdRequest;
+
     @NonNull
-    @Field(id = 4, getter = "getIds")
-    private final List<String> mIds;
-    @NonNull
-    @Field(id = 5, getter = "getUserHandle")
+    @Field(id = 4, getter = "getUserHandle")
     private final UserHandle mUserHandle;
-    @Field(id = 6, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+
+    @Field(id = 5, getter = "getBinderCallStartTimeMillis")
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Removes documents by ID.
      *
      * @param callerAttributionSource The permission identity of the package the document is in.
      * @param databaseName The databaseName the document is in.
-     * @param namespace    Namespace of the document to remove.
-     * @param ids The IDs of the documents to delete
+     * @param removeByDocumentIdRequest The {@link RemoveByDocumentIdRequest} to remove document.
      * @param userHandle Handle of the calling user
      * @param binderCallStartTimeMillis start timestamp of binder call in Millis
      */
@@ -68,14 +71,12 @@
     public RemoveByDocumentIdAidlRequest(
             @Param(id = 1) @NonNull AppSearchAttributionSource callerAttributionSource,
             @Param(id = 2) @NonNull String databaseName,
-            @Param(id = 3) @NonNull String namespace,
-            @Param(id = 4) @NonNull List<String> ids,
-            @Param(id = 5) @NonNull UserHandle userHandle,
-            @Param(id = 6) @ElapsedRealtimeLong long binderCallStartTimeMillis) {
+            @Param(id = 3) @NonNull RemoveByDocumentIdRequest removeByDocumentIdRequest,
+            @Param(id = 4) @NonNull UserHandle userHandle,
+            @Param(id = 5) @ElapsedRealtimeLong long binderCallStartTimeMillis) {
         mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
         mDatabaseName = Objects.requireNonNull(databaseName);
-        mNamespace = Objects.requireNonNull(namespace);
-        mIds = Objects.requireNonNull(ids);
+        mRemoveByDocumentIdRequest = Objects.requireNonNull(removeByDocumentIdRequest);
         mUserHandle = Objects.requireNonNull(userHandle);
         mBinderCallStartTimeMillis = binderCallStartTimeMillis;
     }
@@ -91,13 +92,8 @@
     }
 
     @NonNull
-    public String getNamespace() {
-        return mNamespace;
-    }
-
-    @NonNull
-    public List<String> getIds() {
-        return mIds;
+    public RemoveByDocumentIdRequest getRemoveByDocumentIdRequest() {
+        return mRemoveByDocumentIdRequest;
     }
 
     @NonNull
diff --git a/framework/java/android/app/appsearch/aidl/RemoveByQueryAidlRequest.java b/framework/java/android/app/appsearch/aidl/RemoveByQueryAidlRequest.java
index 3e2952b..eb6bc32 100644
--- a/framework/java/android/app/appsearch/aidl/RemoveByQueryAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/RemoveByQueryAidlRequest.java
@@ -22,38 +22,45 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
-import java.util.List;
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to remove documents by query.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "RemoveByQueryAidlRequestCreator")
 public class RemoveByQueryAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final RemoveByQueryAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<RemoveByQueryAidlRequest> CREATOR =
             new RemoveByQueryAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getQueryExpression")
     private final String mQueryExpression;
+
     @NonNull
     @Field(id = 4, getter = "getSearchSpec")
     private final SearchSpec mSearchSpec;
+
     @NonNull
     @Field(id = 5, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 6, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Removes documents by given query.
diff --git a/framework/java/android/app/appsearch/aidl/ReportUsageAidlRequest.aidl b/framework/java/android/app/appsearch/aidl/ReportUsageAidlRequest.aidl
new file mode 100644
index 0000000..725a0cd
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/ReportUsageAidlRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2024, 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.aidl;
+
+/** {@hide} */
+parcelable ReportUsageAidlRequest;
diff --git a/framework/java/android/app/appsearch/aidl/ReportUsageAidlRequest.java b/framework/java/android/app/appsearch/aidl/ReportUsageAidlRequest.java
new file mode 100644
index 0000000..f0ac432
--- /dev/null
+++ b/framework/java/android/app/appsearch/aidl/ReportUsageAidlRequest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 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.aidl;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.app.appsearch.ReportUsageRequest;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates a request to make a binder call to reports usage of a particular document.
+ *
+ * @hide
+ */
[email protected](creator = "ReportUsageAidlRequestCreator")
+public class ReportUsageAidlRequest extends AbstractSafeParcelable {
+    @NonNull
+    public static final Parcelable.Creator<ReportUsageAidlRequest> CREATOR =
+            new ReportUsageAidlRequestCreator();
+
+    @NonNull
+    @Field(id = 1, getter = "getCallerAttributionSource")
+    private final AppSearchAttributionSource mCallerAttributionSource;
+
+    @NonNull
+    @Field(id = 2, getter = "getTargetPackageName")
+    private final String mTargetPackageName;
+
+    @NonNull
+    @Field(id = 3, getter = "getDatabaseName")
+    private final String mDatabaseName;
+
+    @NonNull
+    @Field(id = 4, getter = "getReportUsageRequest")
+    private final ReportUsageRequest mReportUsageRequest;
+
+    @Field(id = 5, getter = "isSystemUsage")
+    private final boolean mSystemUsage;
+
+    @NonNull
+    @Field(id = 6, getter = "getUserHandle")
+    private final UserHandle mUserHandle;
+
+    @Field(id = 7, getter = "getBinderCallStartTimeMillis")
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
+    /**
+     * Reports usage of a particular document by namespace and id.
+     *
+     * @param callerAttributionSource The permission identity of the package that owns this
+     *     document.
+     * @param targetPackageName The name of the package that owns this document.
+     * @param databaseName The name of the database to report usage against.
+     * @param reportUsageRequest The {@link ReportUsageRequest} to report usage for document.
+     * @param systemUsage Whether the usage was reported by a system app against another app's doc.
+     * @param userHandle Handle of the calling user
+     * @param binderCallStartTimeMillis start timestamp of binder call in Millis
+     */
+    @Constructor
+    public ReportUsageAidlRequest(
+            @Param(id = 1) @NonNull AppSearchAttributionSource callerAttributionSource,
+            @Param(id = 2) @NonNull String targetPackageName,
+            @Param(id = 3) @NonNull String databaseName,
+            @Param(id = 4) @NonNull ReportUsageRequest reportUsageRequest,
+            @Param(id = 5) boolean systemUsage,
+            @Param(id = 6) @NonNull UserHandle userHandle,
+            @Param(id = 7) @ElapsedRealtimeLong long binderCallStartTimeMillis) {
+        mCallerAttributionSource = Objects.requireNonNull(callerAttributionSource);
+        mTargetPackageName = Objects.requireNonNull(targetPackageName);
+        mDatabaseName = Objects.requireNonNull(databaseName);
+        mReportUsageRequest = Objects.requireNonNull(reportUsageRequest);
+        mSystemUsage = systemUsage;
+        mUserHandle = Objects.requireNonNull(userHandle);
+        mBinderCallStartTimeMillis = binderCallStartTimeMillis;
+    }
+
+    @NonNull
+    public AppSearchAttributionSource getCallerAttributionSource() {
+        return mCallerAttributionSource;
+    }
+
+    @NonNull
+    public String getTargetPackageName() {
+        return mTargetPackageName;
+    }
+
+    @NonNull
+    public String getDatabaseName() {
+        return mDatabaseName;
+    }
+
+    @NonNull
+    public ReportUsageRequest getReportUsageRequest() {
+        return mReportUsageRequest;
+    }
+
+    public boolean isSystemUsage() {
+        return mSystemUsage;
+    }
+
+    @NonNull
+    public UserHandle getUserHandle() {
+        return mUserHandle;
+    }
+
+    @ElapsedRealtimeLong
+    public long getBinderCallStartTimeMillis() {
+        return mBinderCallStartTimeMillis;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ReportUsageAidlRequestCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/framework/java/android/app/appsearch/aidl/SearchAidlRequest.java b/framework/java/android/app/appsearch/aidl/SearchAidlRequest.java
index 6052467..c0e6b6f 100644
--- a/framework/java/android/app/appsearch/aidl/SearchAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/SearchAidlRequest.java
@@ -22,6 +22,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -29,30 +30,38 @@
 /**
  * Encapsulates a request to make a binder call to search for documents based on given
  * specifications.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "SearchAidlRequestCreator")
 public class SearchAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final SearchAidlRequestCreator CREATOR = new SearchAidlRequestCreator();
+    public static final Parcelable.Creator<SearchAidlRequest> CREATOR =
+            new SearchAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getSearchExpression")
     private final String mSearchExpression;
+
     @NonNull
     @Field(id = 4, getter = "getSearchSpec")
     private final SearchSpec mSearchSpec;
+
     @NonNull
     @Field(id = 5, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 6, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Searches a document based on a given specifications.
diff --git a/framework/java/android/app/appsearch/aidl/SearchSuggestionAidlRequest.java b/framework/java/android/app/appsearch/aidl/SearchSuggestionAidlRequest.java
index d5bbf4e..e8e3aa5 100644
--- a/framework/java/android/app/appsearch/aidl/SearchSuggestionAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/SearchSuggestionAidlRequest.java
@@ -22,37 +22,45 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to retrieve suggested search strings.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "SearchSuggestionAidlRequestCreator")
 public class SearchSuggestionAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final SearchSuggestionAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<SearchSuggestionAidlRequest> CREATOR =
             new SearchSuggestionAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getSuggestionQueryExpression")
     private final String mSuggestionQueryExpression;
+
     @NonNull
     @Field(id = 4, getter = "getSearchSuggestionSpec")
     private final SearchSuggestionSpec mSearchSuggestionSpec;
+
     @NonNull
     @Field(id = 5, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 6, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Retrieves suggested Strings that could be used as {@code queryExpression} in search API.
diff --git a/framework/java/android/app/appsearch/aidl/SetSchemaAidlRequest.java b/framework/java/android/app/appsearch/aidl/SetSchemaAidlRequest.java
index b1672ba..47950de 100644
--- a/framework/java/android/app/appsearch/aidl/SetSchemaAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/SetSchemaAidlRequest.java
@@ -24,6 +24,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.List;
@@ -32,35 +33,45 @@
 /**
  * Encapsulates a request to make a binder call to update the schema of an {@link AppSearchSession}
  * database.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "SetSchemaAidlRequestCreator")
 public final class SetSchemaAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final SetSchemaAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<SetSchemaAidlRequest> CREATOR =
             new SetSchemaAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getSchemas")
     private final List<AppSearchSchema> mSchemas;
+
     @NonNull
     @Field(id = 4, getter = "getVisibilityConfigs")
     private final List<InternalVisibilityConfig> mVisibilityConfigs;
+
     @Field(id = 5, getter = "isForceOverride")
     private final boolean mForceOverride;
+
     @Field(id = 6, getter = "getSchemaVersion")
     private final int mSchemaVersion;
+
     @NonNull
     @Field(id = 7, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 8, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
+
     @Field(id = 9, getter = "getSchemaMigrationCallType")
     private final int mSchemaMigrationCallType;
 
@@ -68,13 +79,13 @@
      * Updates the AppSearch schema for this database.
      *
      * @param callerAttributionSource The permission identity of the package that owns this schema.
-     * @param databaseName  The name of the database where this schema lives.
+     * @param databaseName The name of the database where this schema lives.
      * @param schemas List of {@link AppSearchSchema} objects.
      * @param visibilityConfigs List of {@link InternalVisibilityConfig} objects defining the
      *     visibility for the schema types.
      * @param forceOverride Whether to apply the new schema even if it is incompatible. All
      *     incompatible documents will be deleted.
-     * @param schemaVersion  The overall schema version number of the request.
+     * @param schemaVersion The overall schema version number of the request.
      * @param userHandle Handle of the calling user
      * @param binderCallStartTimeMillis start timestamp of binder call in Millis
      * @param schemaMigrationCallType Indicates how a SetSchema call relative to SchemaMigration
diff --git a/framework/java/android/app/appsearch/aidl/UnregisterObserverCallbackAidlRequest.java b/framework/java/android/app/appsearch/aidl/UnregisterObserverCallbackAidlRequest.java
index a0bef67..0f16469 100644
--- a/framework/java/android/app/appsearch/aidl/UnregisterObserverCallbackAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/UnregisterObserverCallbackAidlRequest.java
@@ -22,31 +22,37 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
 
 /**
  * Encapsulates a request to make a binder call to remove a previously registered observer.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "UnregisterObserverCallbackAidlRequestCreator")
 public class UnregisterObserverCallbackAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final UnregisterObserverCallbackAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<UnregisterObserverCallbackAidlRequest> CREATOR =
             new UnregisterObserverCallbackAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getObservedPackage")
     private final String mObservedPackage;
+
     @NonNull
     @Field(id = 3, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 4, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Removes previously registered {@link ObserverCallback} instances from the system.
diff --git a/framework/java/android/app/appsearch/aidl/ValueParcel.java b/framework/java/android/app/appsearch/aidl/ValueParcel.java
deleted file mode 100644
index 6849617..0000000
--- a/framework/java/android/app/appsearch/aidl/ValueParcel.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024 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.aidl;
-
-import android.annotation.NonNull;
-import android.app.appsearch.AppSearchResult;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * Parcelable wrapper around {@link AppSearchResult}'s value.
- *
- * @param <ValueType> The type of result object for successful calls. Must be a parcelable type.
- * @hide
- */
-public final class ValueParcel<ValueType> implements Parcelable {
-
-    private final ValueType mValue;
-
-    private ValueParcel(@NonNull Parcel in) {
-        mValue = (ValueType) in.readValue(ValueParcel.class.getClassLoader());
-    }
-
-    public ValueParcel(ValueType value) {
-        mValue = value;
-    }
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeValue(mValue);
-    }
-
-    public ValueType getValue() {
-        return mValue;
-    }
-
-    @NonNull
-    public static final Creator<ValueParcel<?>> CREATOR =
-            new Creator<>() {
-                @NonNull
-                @Override
-                public ValueParcel<?> createFromParcel(@NonNull Parcel in) {
-                    return new ValueParcel<>(in);
-                }
-
-                @NonNull
-                @Override
-                public ValueParcel<?>[] newArray(int size) {
-                    return new ValueParcel<?>[size];
-                }
-            };
-}
-
diff --git a/framework/java/android/app/appsearch/aidl/WriteSearchResultsToFileAidlRequest.java b/framework/java/android/app/appsearch/aidl/WriteSearchResultsToFileAidlRequest.java
index a57e8de..51c9706 100644
--- a/framework/java/android/app/appsearch/aidl/WriteSearchResultsToFileAidlRequest.java
+++ b/framework/java/android/app/appsearch/aidl/WriteSearchResultsToFileAidlRequest.java
@@ -23,6 +23,7 @@
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.UserHandle;
 
 import java.util.Objects;
@@ -30,34 +31,42 @@
 /**
  * Encapsulates a request to make a binder call to search for documents based on the given
  * specifications and save the results to the given {@link ParcelFileDescriptor}.
+ *
  * @hide
  */
 @SafeParcelable.Class(creator = "WriteSearchResultsToFileAidlRequestCreator")
 public class WriteSearchResultsToFileAidlRequest extends AbstractSafeParcelable {
     @NonNull
-    public static final WriteSearchResultsToFileAidlRequestCreator CREATOR =
+    public static final Parcelable.Creator<WriteSearchResultsToFileAidlRequest> CREATOR =
             new WriteSearchResultsToFileAidlRequestCreator();
 
     @NonNull
     @Field(id = 1, getter = "getCallerAttributionSource")
     private final AppSearchAttributionSource mCallerAttributionSource;
+
     @NonNull
     @Field(id = 2, getter = "getDatabaseName")
     private final String mDatabaseName;
+
     @NonNull
     @Field(id = 3, getter = "getParcelFileDescriptor")
     private final ParcelFileDescriptor mParcelFileDescriptor;
+
     @NonNull
     @Field(id = 4, getter = "getSearchExpression")
     private final String mSearchExpression;
+
     @NonNull
     @Field(id = 5, getter = "getSearchSpec")
     private final SearchSpec mSearchSpec;
+
     @NonNull
     @Field(id = 6, getter = "getUserHandle")
     private final UserHandle mUserHandle;
+
     @Field(id = 7, getter = "getBinderCallStartTimeMillis")
-    private final @ElapsedRealtimeLong long mBinderCallStartTimeMillis;
+    @ElapsedRealtimeLong
+    private final long mBinderCallStartTimeMillis;
 
     /**
      * Searches a document based on a given specifications.
diff --git a/framework/java/android/app/appsearch/functions/AppFunctionManager.java b/framework/java/android/app/appsearch/functions/AppFunctionManager.java
new file mode 100644
index 0000000..fa916dc
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/AppFunctionManager.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.UserHandleAware;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.SearchSessionUtil;
+import android.app.appsearch.aidl.AppSearchAttributionSource;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.ExecuteAppFunctionAidlRequest;
+import android.app.appsearch.aidl.IAppSearchManager;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.content.Context;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import com.android.appsearch.flags.Flags;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Provides app functions related functionalities.
+ *
+ * <p>App function is a specific piece of functionality that an app offers to the system. These
+ * functionalities can be integrated into various system features.
+ *
+ * <p>You can obtain an instance using {@link AppSearchManager#getAppFunctionManager()}.
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+public final class AppFunctionManager {
+    /**
+     * Allows system applications to execute app functions provided by apps through AppSearch.
+     *
+     * <p>Protection level: internal|role.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String PERMISSION_EXECUTE_APP_FUNCTION =
+            "android.permission.EXECUTE_APP_FUNCTION";
+
+    /**
+     * Must be required by a {@link android.app.appsearch.functions.AppFunctionService}, to ensure
+     * that only the system can bind to it.
+     *
+     * <p>Protection level: signature.
+     */
+    public static final String PERMISSION_BIND_APP_FUNCTION_SERVICE =
+            "android.permission.BIND_APP_FUNCTION_SERVICE";
+
+    private final IAppSearchManager mService;
+    private final Context mContext;
+
+    /** @hide */
+    public AppFunctionManager(@NonNull Context context, @NonNull IAppSearchManager service) {
+        mContext = Objects.requireNonNull(context);
+        mService = Objects.requireNonNull(service);
+    }
+
+    /**
+     * Executes an app function provided by {@link AppFunctionService} through the system.
+     *
+     * @param request The request.
+     * @param executor Executor on which to invoke the callback.
+     * @param callback A callback to receive the function execution result.
+     */
+    @UserHandleAware
+    public void executeAppFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<AppSearchResult<ExecuteAppFunctionResponse>> callback) {
+        Objects.requireNonNull(request);
+        Objects.requireNonNull(callback);
+
+        ExecuteAppFunctionAidlRequest aidlRequest =
+                new ExecuteAppFunctionAidlRequest(
+                        request,
+                        AppSearchAttributionSource.createAttributionSource(
+                                mContext, /* callingPid= */ Process.myPid()),
+                        mContext.getUser(),
+                        SystemClock.elapsedRealtime());
+        try {
+            mService.executeAppFunction(
+                    aidlRequest,
+                    new IAppSearchResultCallback.Stub() {
+                        @Override
+                        public void onResult(AppSearchResultParcel result) {
+                            SearchSessionUtil.safeExecute(
+                                    executor, callback, () -> callback.accept(result.getResult()));
+                        }
+                    });
+        } catch (RemoteException ex) {
+            ex.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/framework/java/android/app/appsearch/functions/AppFunctionService.java b/framework/java/android/app/appsearch/functions/AppFunctionService.java
new file mode 100644
index 0000000..9d9441c
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/AppFunctionService.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.FlaggedApi;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.IAppFunctionService;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+
+import com.android.appsearch.flags.Flags;
+
+import java.util.function.Consumer;
+
+/**
+ * Abstract base class to provide app functions to the system.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourService"
+ *      android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
+ *    <intent-filter>
+ *      <action android:name="android.app.appsearch.functions.AppFunctionService" />
+ *    </intent-filter>
+ * </service>
+ * }
+ * </pre>
+ *
+ * @see AppFunctionManager
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+public abstract class AppFunctionService extends Service {
+    private static final String TAG = "AppSearchAppFunction";
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service. To be supported, the
+     * service must also require the {@link AppFunctionManager#PERMISSION_BIND_APP_FUNCTION_SERVICE}
+     * permission so that other applications can not abuse it.
+     */
+    @NonNull
+    public static final String SERVICE_INTERFACE =
+            "android.app.appsearch.functions.AppFunctionService";
+
+    private final Binder mBinder =
+            new IAppFunctionService.Stub() {
+                @Override
+                public void executeAppFunction(
+                        @NonNull ExecuteAppFunctionRequest request,
+                        @NonNull IAppSearchResultCallback callback) {
+                    // TODO(b/327134039): Replace this check with the new permission
+                    if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+                        throw new SecurityException("Can only be called by the system server");
+                    }
+                    SafeOneTimeAppSearchResultCallback safeCallback =
+                            new SafeOneTimeAppSearchResultCallback(callback);
+                    try {
+                        AppFunctionService.this.onExecuteFunction(
+                                request,
+                                appFunctionResult -> {
+                                    AppSearchResultParcel appSearchResultParcel;
+                                    // Create result from value in success case and errorMessage in
+                                    // failure case.
+                                    if (appFunctionResult.isSuccess()) {
+                                        appSearchResultParcel =
+                                                AppSearchResultParcel
+                                                        .fromExecuteAppFunctionResponse(
+                                                                appFunctionResult.getResultValue());
+                                    } else {
+                                        appSearchResultParcel =
+                                                AppSearchResultParcel.fromFailedResult(
+                                                        appFunctionResult);
+                                    }
+                                    safeCallback.onResult(appSearchResultParcel);
+                                });
+                    } catch (Exception ex) {
+                        // Apps should handle exceptions. But if they don't, report the error on
+                        // behalf of them.
+                        AppSearchResult failedResult = AppSearchResult.throwableToFailedResult(ex);
+                        safeCallback.onResult(AppSearchResultParcel.fromFailedResult(failedResult));
+                    }
+                }
+            };
+
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return mBinder;
+    }
+
+    /**
+     * Called by the system to execute a specific app function.
+     *
+     * <p>This method is triggered when the system requests your AppFunctionService to handle a
+     * particular function you have registered and made available.
+     *
+     * <p>To ensure proper routing of function requests, assign a unique identifier to each
+     * function. This identifier doesn't need to be globally unique, but it must be unique within
+     * your app. For example, a function to order food could be identified as "orderFood". You can
+     * determine the specific function to invoke by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     *
+     * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+     * thread and dispatch the result with the given callback. You should always report back the
+     * result using the callback, no matter if the execution was successful or not.
+     *
+     * @param request The function execution request.
+     * @param callback A callback to report back the result.
+     */
+    @MainThread
+    public abstract void onExecuteFunction(
+            @NonNull ExecuteAppFunctionRequest request,
+            @NonNull Consumer<AppSearchResult<ExecuteAppFunctionResponse>> callback);
+}
diff --git a/framework/java/android/app/appsearch/functions/ExecuteAppFunctionRequest.aidl b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionRequest.aidl
new file mode 100644
index 0000000..e2601cc
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionRequest.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2024, 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.functions;
+
+/** {@hide} */
+parcelable ExecuteAppFunctionRequest;
diff --git a/framework/java/android/app/appsearch/functions/ExecuteAppFunctionRequest.java b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionRequest.java
new file mode 100644
index 0000000..80dd66f
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionRequest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.GenericDocumentParcel;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.appsearch.flags.Flags;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Represents a request to execute a specific app function.
+ *
+ * @see AppFunctionManager#executeAppFunction(ExecuteAppFunctionRequest, Executor, Consumer)
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
[email protected](creator = "ExecuteAppFunctionRequestCreator")
+public final class ExecuteAppFunctionRequest extends AbstractSafeParcelable implements Parcelable {
+    @NonNull
+    public static final Parcelable.Creator<ExecuteAppFunctionRequest> CREATOR =
+            new ExecuteAppFunctionRequestCreator();
+
+    @Field(id = 1, getter = "getTargetPackageName")
+    @NonNull
+    private final String mTargetPackageName;
+
+    @Field(id = 2, getter = "getFunctionIdentifier")
+    @NonNull
+    private final String mFunctionIdentifier;
+
+    /**
+     * {@link GenericDocument} is not a Parcelable, so storing it as a GenericDocumentParcel here.
+     */
+    @Field(id = 3)
+    @NonNull
+    final GenericDocumentParcel mParameters;
+
+    @Field(id = 4, getter = "getExtras")
+    @NonNull
+    private final Bundle mExtras;
+
+    @Field(id = 5, getter = "getSha256Certificate")
+    @Nullable
+    private final byte[] mSha256Certificate;
+
+    @NonNull private final GenericDocument mParametersCached;
+
+    /** Returns the package name of the app that hosts the function. */
+    @NonNull
+    public String getTargetPackageName() {
+        return mTargetPackageName;
+    }
+
+    /** Returns the unique string identifier of the app function to be executed. */
+    @NonNull
+    public String getFunctionIdentifier() {
+        return mFunctionIdentifier;
+    }
+
+    /**
+     * Returns the parameters required to invoke this function. Within this {@link GenericDocument},
+     * the property names are the names of the function parameters and the property values are the
+     * values of those parameters
+     *
+     * <p>The document may have missing parameters. Developers are advised to implement defensive
+     * handling measures.
+     */
+    @NonNull
+    public GenericDocument getParameters() {
+        return mParametersCached;
+    }
+
+    /**
+     * Returns the expected certificate SHA-256 digests of the target package. Returns {@code null}
+     * if no certificate digest checking is configured.
+     *
+     * @see Builder#getSha256Certificate()
+     */
+    @Nullable
+    public byte[] getSha256Certificate() {
+        return mSha256Certificate;
+    }
+
+    /** Returns additional metadata relevant to this function execution request. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    private ExecuteAppFunctionRequest(
+            @NonNull String targetPackageName,
+            @NonNull String functionIdentifier,
+            @NonNull GenericDocument document,
+            @NonNull Bundle extras,
+            @Nullable byte[] sha256Certificate) {
+        mTargetPackageName = Objects.requireNonNull(targetPackageName);
+        mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        mParametersCached = Objects.requireNonNull(document);
+        mParameters = mParametersCached.getDocumentParcel();
+        mExtras = Objects.requireNonNull(extras);
+        mSha256Certificate = sha256Certificate;
+    }
+
+    @Constructor
+    ExecuteAppFunctionRequest(
+            @Param(id = 1) @NonNull String targetPackageName,
+            @Param(id = 2) @NonNull String functionIdentifier,
+            @Param(id = 3) @NonNull GenericDocumentParcel parameters,
+            @Param(id = 4) @NonNull Bundle extras,
+            @Param(id = 5) @Nullable byte[] sha256Certificate) {
+        mTargetPackageName = Objects.requireNonNull(targetPackageName);
+        mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        mParameters = Objects.requireNonNull(parameters);
+        mParametersCached = new GenericDocument(mParameters);
+        mExtras = Objects.requireNonNull(extras);
+        mSha256Certificate = sha256Certificate;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ExecuteAppFunctionRequestCreator.writeToParcel(this, dest, flags);
+    }
+
+    /** The builder for creating {@link ExecuteAppFunctionRequest} instances. */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    public static final class Builder {
+        @NonNull private final String mPackageName;
+        @NonNull private final String mFunctionIdentifier;
+        @NonNull private GenericDocument mParameters = GenericDocument.EMPTY;
+        @NonNull private Bundle mExtras = Bundle.EMPTY;
+        @Nullable private byte[] mSha256Certificate;
+
+        /**
+         * Creates a new instance of this builder class.
+         *
+         * @param packageName The package name of the target app providing the app function to
+         *     invoke.
+         * @param functionIdentifier The identifier used by the {@link AppFunctionService} from the
+         *     target app to uniquely identify the function to be invoked.
+         */
+        public Builder(@NonNull String packageName, @NonNull String functionIdentifier) {
+            mPackageName = Objects.requireNonNull(packageName);
+            mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+        }
+
+        /**
+         * Sets parameters for invoking the app function. Within this {@link GenericDocument}, the
+         * property names are the names of the function parameters and the property values are the
+         * values of those parameters. Defaults to an empty {@link GenericDocument} if not set.
+         */
+        @NonNull
+        public Builder setParameters(@NonNull GenericDocument parameters) {
+            mParameters = parameters;
+            return this;
+        }
+
+        /**
+         * Sets the expected certificate SHA-256 digests for the target package. Setting this to
+         * {@code null} indicates that no certificate digest check will be performed.
+         *
+         * <p>SHA-256 certificate digests for a signed application can be retrieved with the <a
+         * href="{@docRoot}studio/command-line/apksigner/">apksigner tool</a> that is part of the
+         * Android SDK build tools. Use {@code apksigner verify --print-certs path/to/apk.apk} to
+         * retrieve the SHA-256 certificate digest for the target application. Once retrieved, the
+         * SHA-256 certificate digest should be converted to a {@code byte[]} by decoding it in
+         * base16:
+         *
+         * <pre>
+         * new android.content.pm.Signature(outputDigest).toByteArray();
+         * </pre>
+         */
+        @NonNull
+        public Builder setSha256Certificate(@Nullable byte[] sha256Certificate) {
+            mSha256Certificate = sha256Certificate;
+            return this;
+        }
+
+        /**
+         * Sets the additional metadata relevant to this function execution request. Defaults to an
+         * empty {@link Bundle} if not set.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /** Constructs a new {@link ExecuteAppFunctionRequest} from the contents of this builder. */
+        @NonNull
+        public ExecuteAppFunctionRequest build() {
+            return new ExecuteAppFunctionRequest(
+                    mPackageName, mFunctionIdentifier, mParameters, mExtras, mSha256Certificate);
+        }
+    }
+}
diff --git a/framework/java/android/app/appsearch/functions/ExecuteAppFunctionResponse.aidl b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionResponse.aidl
new file mode 100644
index 0000000..7ed9ab9
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionResponse.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2024, 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.functions;
+
+/** {@hide} */
+parcelable ExecuteAppFunctionResponse;
diff --git a/framework/java/android/app/appsearch/functions/ExecuteAppFunctionResponse.java b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionResponse.java
new file mode 100644
index 0000000..7a6e188
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/ExecuteAppFunctionResponse.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.GenericDocumentParcel;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.appsearch.flags.Flags;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Represents a response of an execution of an app function.
+ *
+ * @see AppFunctionManager#executeAppFunction(ExecuteAppFunctionRequest, Executor, Consumer)
+ */
[email protected](creator = "ExecuteAppFunctionResponseCreator")
+@FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+public final class ExecuteAppFunctionResponse extends AbstractSafeParcelable {
+    /**
+     * The name of the property that stores the result within the result {@link GenericDocument}.
+     *
+     * @see #getResult().
+     */
+    public static final String PROPERTY_RESULT = "result";
+
+    @NonNull
+    public static final Parcelable.Creator<ExecuteAppFunctionResponse> CREATOR =
+            new ExecuteAppFunctionResponseCreator();
+
+    @Field(id = 1)
+    @NonNull
+    final GenericDocumentParcel mResult;
+
+    @Field(id = 2, getter = "getExtras")
+    @NonNull
+    private final Bundle mExtras;
+
+    @NonNull private final GenericDocument mResultCached;
+
+    @Constructor
+    ExecuteAppFunctionResponse(
+            @Param(id = 1) @NonNull GenericDocumentParcel result,
+            @Param(id = 2) @NonNull Bundle extras) {
+        mResult = Objects.requireNonNull(result);
+        mResultCached = new GenericDocument(mResult);
+        mExtras = extras;
+    }
+
+    private ExecuteAppFunctionResponse(@NonNull GenericDocument result, @NonNull Bundle extras) {
+        mResultCached = Objects.requireNonNull(result);
+        mResult = mResultCached.getDocumentParcel();
+        mExtras = Objects.requireNonNull(extras);
+    }
+
+    /**
+     * Returns the return value of the executed function. An empty document indicates that the
+     * function does not produce a return value.
+     */
+    @NonNull
+    public GenericDocument getResult() {
+        return mResultCached;
+    }
+
+    /** Returns the additional metadata data relevant to this function execution response. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ExecuteAppFunctionResponseCreator.writeToParcel(this, dest, flags);
+    }
+
+    /** The builder for creating {@link ExecuteAppFunctionResponse} instances. */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    public static final class Builder {
+        @NonNull private GenericDocument mResult = GenericDocument.EMPTY;
+        @NonNull private Bundle mExtras = Bundle.EMPTY;
+
+        /**
+         * Sets the result of the app function execution. The result is stored within a {@link
+         * GenericDocument} under the property name {@link #PROPERTY_RESULT}. An empty {@link
+         * GenericDocument} indicates that the function does not produce a return value. Defaults to
+         * an empty {@link GenericDocument} if not set.
+         */
+        @NonNull
+        public Builder setResult(@NonNull GenericDocument result) {
+            mResult = result;
+            return this;
+        }
+
+        /**
+         * Sets the additional metadata relevant to this function execution response. Defaults to
+         * {@link Bundle#EMPTY} if not set.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Constructs a new {@link ExecuteAppFunctionResponse} from the contents of this builder.
+         */
+        @NonNull
+        public ExecuteAppFunctionResponse build() {
+            return new ExecuteAppFunctionResponse(mResult, mExtras);
+        }
+    }
+}
diff --git a/framework/java/android/app/appsearch/functions/SafeOneTimeAppSearchResultCallback.java b/framework/java/android/app/appsearch/functions/SafeOneTimeAppSearchResultCallback.java
new file mode 100644
index 0000000..7b0196d
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/SafeOneTimeAppSearchResultCallback.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.IAppSearchResultCallback;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+/**
+ * A wrapper of IAppSearchResultCallback which swallows the {@link RemoteException}. This callback
+ * is intended for one-time use only. Subsequent calls to onResult() will be ignored.
+ *
+ * @hide
+ */
+public class SafeOneTimeAppSearchResultCallback {
+    private static final String TAG = "AppSearchAppFunction";
+
+    private final AtomicBoolean mOnResultCalled = new AtomicBoolean(false);
+
+    @NonNull private final IAppSearchResultCallback mCallback;
+
+    @Nullable private final Consumer<AppSearchResult<?>> mOnDispatchCallback;
+
+    public SafeOneTimeAppSearchResultCallback(@NonNull IAppSearchResultCallback callback) {
+        this(callback, /* onDispatchCallback= */ null);
+    }
+
+    /**
+     * @param callback The callback to wrap.
+     * @param onDispatchCallback An optional callback invoked after the wrapped callback has been
+     *     dispatched with a result. This callback receives the result that has been dispatched.
+     */
+    public SafeOneTimeAppSearchResultCallback(
+            @NonNull IAppSearchResultCallback callback,
+            @Nullable Consumer<AppSearchResult<?>> onDispatchCallback) {
+        mCallback = Objects.requireNonNull(callback);
+        mOnDispatchCallback = onDispatchCallback;
+    }
+
+    public void onFailedResult(@NonNull AppSearchResult<?> result) {
+        onResult(AppSearchResultParcel.fromFailedResult(result));
+    }
+
+    public void onResult(@NonNull AppSearchResultParcel<?> result) {
+        if (!mOnResultCalled.compareAndSet(false, true)) {
+            Log.w(TAG, "Ignore subsequent calls to onResult()");
+            return;
+        }
+        try {
+            mCallback.onResult(result);
+        } catch (RemoteException ex) {
+            // Failed to notify the other end. Ignore.
+            Log.w(TAG, "Failed to invoke the callback", ex);
+        }
+        if (mOnDispatchCallback != null) {
+            mOnDispatchCallback.accept(result.getResult());
+        }
+    }
+}
diff --git a/framework/java/android/app/appsearch/functions/ServiceCallHelper.java b/framework/java/android/app/appsearch/functions/ServiceCallHelper.java
new file mode 100644
index 0000000..8ae262d
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/ServiceCallHelper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.os.UserHandle;
+
+/**
+ * Defines a contract for establishing temporary connections to services and executing operations
+ * within a specified timeout. Implementations of this interface provide mechanisms to ensure that
+ * services are properly unbound after the operation completes or a timeout occurs.
+ *
+ * @hide
+ */
+public interface ServiceCallHelper<T> {
+
+    /**
+     * Initiates service binding and executes a provided method when the service connects. Unbinds
+     * the service after execution or upon timeout. Returns the result of the bindService API.
+     *
+     * <p>When the service connection was made successfully, it's the caller responsibility to
+     * report the usage is completed and can be unbound by calling {@link
+     * ServiceUsageCompleteListener#onCompleted()}.
+     *
+     * <p>This method includes a timeout mechanism to prevent the system from being stuck in a state
+     * where a service is bound indefinitely (for example, if the binder method never returns). This
+     * helps ensure that the calling app does not remain alive unnecessarily.
+     *
+     * @param intent An Intent object that describes the service that should be bound.
+     * @param bindFlags Flags used to control the binding process See {@link
+     *     android.content.Context#bindService}.
+     * @param timeoutInMillis The maximum time in milliseconds to wait for the service connection.
+     * @param userHandle The UserHandle of the user for which the service should be bound.
+     * @param callback A callback to be invoked for various events. See {@link
+     *     RunServiceCallCallback}.
+     */
+    boolean runServiceCall(
+            @NonNull Intent intent,
+            int bindFlags,
+            long timeoutInMillis,
+            @NonNull UserHandle userHandle,
+            @NonNull RunServiceCallCallback<T> callback);
+
+    /** An interface for clients to signal that they have finished using a bound service. */
+    interface ServiceUsageCompleteListener {
+        /**
+         * Called when a client has finished using a bound service. This indicates that the service
+         * can be safely unbound.
+         */
+        void onCompleted();
+    }
+
+    interface RunServiceCallCallback<T> {
+        /**
+         * Called when the service connection has been established. Uses {@code
+         * serviceUsageCompleteListener} to report finish using the connected service.
+         */
+        void onServiceConnected(
+                @NonNull T service,
+                @NonNull ServiceUsageCompleteListener serviceUsageCompleteListener);
+
+        /** Called when the service connection was failed to establish. */
+        void onFailedToConnect();
+
+        /**
+         * Called when the whole operation(i.e. binding and the service call) takes longer than
+         * allowed.
+         */
+        void onTimedOut();
+    }
+}
diff --git a/framework/java/android/app/appsearch/functions/ServiceCallHelperImpl.java b/framework/java/android/app/appsearch/functions/ServiceCallHelperImpl.java
new file mode 100644
index 0000000..a89e76b
--- /dev/null
+++ b/framework/java/android/app/appsearch/functions/ServiceCallHelperImpl.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 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.functions;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.Log;
+
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * An implementation of {@link ServiceCallHelper} that that is based on {@link Context#bindService}.
+ *
+ * @hide
+ */
+public class ServiceCallHelperImpl<T> implements ServiceCallHelper<T> {
+    private static final String TAG = "AppSearchAppFunction";
+
+    @NonNull private final Context mContext;
+    @NonNull private final Function<IBinder, T> mInterfaceConverter;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Executor mExecutor;
+
+    /**
+     * @param interfaceConverter A function responsible for converting an IBinder object into the
+     *     desired service interface.
+     * @param executor An Executor instance to dispatch callback.
+     * @param context The system context.
+     */
+    public ServiceCallHelperImpl(
+            @NonNull Context context,
+            @NonNull Function<IBinder, T> interfaceConverter,
+            @NonNull Executor executor) {
+        mContext = context;
+        mInterfaceConverter = interfaceConverter;
+        mExecutor = executor;
+    }
+
+    @Override
+    public boolean runServiceCall(
+            @NonNull Intent intent,
+            int bindFlags,
+            long timeoutInMillis,
+            @NonNull UserHandle userHandle,
+            @NonNull RunServiceCallCallback<T> callback) {
+        OneOffServiceConnection serviceConnection =
+                new OneOffServiceConnection(
+                        intent, bindFlags, timeoutInMillis, userHandle, callback);
+
+        return serviceConnection.bindAndRun();
+    }
+
+    private class OneOffServiceConnection
+            implements ServiceConnection, ServiceUsageCompleteListener {
+        private final Intent mIntent;
+        private final int mFlags;
+        private final long mTimeoutMillis;
+        private final UserHandle mUserHandle;
+        private final RunServiceCallCallback<T> mCallback;
+        private final Runnable mTimeoutCallback;
+
+        OneOffServiceConnection(
+                @NonNull Intent intent,
+                int flags,
+                long timeoutMillis,
+                @NonNull UserHandle userHandle,
+                @NonNull RunServiceCallCallback<T> callback) {
+            mIntent = intent;
+            mFlags = flags;
+            mTimeoutMillis = timeoutMillis;
+            mCallback = callback;
+            mTimeoutCallback =
+                    () ->
+                            mExecutor.execute(
+                                    () -> {
+                                        safeUnbind();
+                                        mCallback.onTimedOut();
+                                    });
+            mUserHandle = userHandle;
+        }
+
+        public boolean bindAndRun() {
+            boolean bindServiceResult =
+                    mContext.bindServiceAsUser(mIntent, this, mFlags, mUserHandle);
+
+            if (bindServiceResult) {
+                mHandler.postDelayed(mTimeoutCallback, mTimeoutMillis);
+            } else {
+                safeUnbind();
+            }
+
+            return bindServiceResult;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            T serviceInterface = mInterfaceConverter.apply(service);
+
+            mExecutor.execute(() -> mCallback.onServiceConnected(serviceInterface, this));
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            safeUnbind();
+            mExecutor.execute(mCallback::onFailedToConnect);
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            safeUnbind();
+            mExecutor.execute(mCallback::onFailedToConnect);
+        }
+
+        @Override
+        public void onNullBinding(ComponentName name) {
+            safeUnbind();
+            mExecutor.execute(mCallback::onFailedToConnect);
+        }
+
+        private void safeUnbind() {
+            try {
+                mHandler.removeCallbacks(mTimeoutCallback);
+                mContext.unbindService(this);
+            } catch (Exception ex) {
+                Log.w(TAG, "Failed to unbind", ex);
+            }
+        }
+
+        @Override
+        public void onCompleted() {
+            safeUnbind();
+        }
+    }
+}
diff --git a/framework/java/android/app/appsearch/safeparcel/AbstractSafeParcelable.java b/framework/java/android/app/appsearch/safeparcel/AbstractSafeParcelable.java
index 7cc0655..fecfc6a 100644
--- a/framework/java/android/app/appsearch/safeparcel/AbstractSafeParcelable.java
+++ b/framework/java/android/app/appsearch/safeparcel/AbstractSafeParcelable.java
@@ -17,8 +17,8 @@
 package android.app.appsearch.safeparcel;
 
 import android.annotation.FlaggedApi;
-import android.app.appsearch.flags.Flags;
 
+import com.android.appsearch.flags.Flags;
 
 /**
  * Implements {@link SafeParcelable} and implements some default methods defined by {@link
diff --git a/framework/java/android/app/appsearch/stats/SchemaMigrationStats.aidl b/framework/java/android/app/appsearch/stats/SchemaMigrationStats.aidl
index c85f47d..87f62d8 100644
--- a/framework/java/android/app/appsearch/stats/SchemaMigrationStats.aidl
+++ b/framework/java/android/app/appsearch/stats/SchemaMigrationStats.aidl
@@ -16,4 +16,4 @@
 package android.app.appsearch.stats;
 
 /** {@hide} */
-parcelable SchemaMigrationStats;
\ No newline at end of file
+parcelable SchemaMigrationStats;
diff --git a/framework/java/android/app/appsearch/util/ExceptionUtil.java b/framework/java/android/app/appsearch/util/ExceptionUtil.java
new file mode 100644
index 0000000..ab72576
--- /dev/null
+++ b/framework/java/android/app/appsearch/util/ExceptionUtil.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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.util;
+
+import android.annotation.NonNull;
+import android.os.RemoteException;
+
+/**
+ * Utilities for handling exceptions.
+ *
+ * @hide
+ */
+public final class ExceptionUtil {
+
+    /**
+     * {@link RuntimeException} will be rethrown if {@link #isItOkayToRethrowException()} returns
+     * true.
+     */
+    public static void handleException(@NonNull Exception e) {
+        if (isItOkayToRethrowException() && e instanceof RuntimeException) {
+            rethrowRuntimeException((RuntimeException) e);
+        }
+    }
+
+    /** Returns whether it is OK to rethrow exceptions from this entrypoint. */
+    private static boolean isItOkayToRethrowException() {
+        return false;
+    }
+
+    /** Rethrow exception from SystemServer in Framework code. */
+    public static void handleRemoteException(@NonNull RemoteException e) {
+        e.rethrowFromSystemServer();
+    }
+
+    /**
+     * A helper method to rethrow {@link RuntimeException}.
+     *
+     * <p>We use this to enforce exception type and assure the compiler/linter that the exception is
+     * indeed {@link RuntimeException} and can be rethrown safely.
+     */
+    private static void rethrowRuntimeException(RuntimeException e) {
+        throw e;
+    }
+
+    private ExceptionUtil() {}
+}
diff --git a/framework/java/external/android/app/appsearch/AppSearchBatchResult.java b/framework/java/external/android/app/appsearch/AppSearchBatchResult.java
index efd5e31..ac89af5 100644
--- a/framework/java/external/android/app/appsearch/AppSearchBatchResult.java
+++ b/framework/java/external/android/app/appsearch/AppSearchBatchResult.java
@@ -42,12 +42,17 @@
  * @see AppSearchSession#remove
  */
 public final class AppSearchBatchResult<KeyType, ValueType> {
-    @NonNull private final Map<KeyType, ValueType> mSuccesses;
+    @NonNull
+    private final Map<KeyType, @android.app.appsearch.checker.nullness.qual.Nullable ValueType>
+            mSuccesses;
+
     @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mFailures;
     @NonNull private final Map<KeyType, AppSearchResult<ValueType>> mAll;
 
     AppSearchBatchResult(
-            @NonNull Map<KeyType, ValueType> successes,
+            @NonNull
+                    Map<KeyType, @android.app.appsearch.checker.nullness.qual.Nullable ValueType>
+                            successes,
             @NonNull Map<KeyType, AppSearchResult<ValueType>> failures,
             @NonNull Map<KeyType, AppSearchResult<ValueType>> all) {
         mSuccesses = Objects.requireNonNull(successes);
@@ -121,7 +126,8 @@
      * @param <ValueType> The type of the result objects for successful results.
      */
     public static final class Builder<KeyType, ValueType> {
-        private ArrayMap<KeyType, ValueType> mSuccesses = new ArrayMap<>();
+        private ArrayMap<KeyType, @android.app.appsearch.checker.nullness.qual.Nullable ValueType>
+                mSuccesses = new ArrayMap<>();
         private ArrayMap<KeyType, AppSearchResult<ValueType>> mFailures = new ArrayMap<>();
         private ArrayMap<KeyType, AppSearchResult<ValueType>> mAll = new ArrayMap<>();
         private boolean mBuilt = false;
diff --git a/framework/java/external/android/app/appsearch/AppSearchEnvironment.java b/framework/java/external/android/app/appsearch/AppSearchEnvironment.java
new file mode 100644
index 0000000..f6a68be
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/AppSearchEnvironment.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.UserHandle;
+
+import java.io.File;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An interface which exposes environment specific methods for AppSearch.
+ *
+ * @hide
+ */
+public interface AppSearchEnvironment {
+
+    /** Returns the directory to initialize appsearch based on the environment. */
+    @NonNull
+    File getAppSearchDir(@NonNull Context context, @Nullable UserHandle userHandle);
+
+    /** Returns the correct context for the user based on the environment. */
+    @NonNull
+    Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle);
+
+    /** Returns an ExecutorService based on given parameters. */
+    @NonNull
+    ExecutorService createExecutorService(
+            int corePoolSize,
+            int maxConcurrency,
+            long keepAliveTime,
+            @NonNull TimeUnit unit,
+            @NonNull BlockingQueue<Runnable> workQueue,
+            int priority);
+
+    /** Returns an ExecutorService with a single thread. */
+    @NonNull
+    ExecutorService createSingleThreadExecutor();
+
+    /** Creates and returns an Executor with cached thread pools. */
+    @NonNull
+    ExecutorService createCachedThreadPoolExecutor();
+
+    /**
+     * Returns a cache directory for creating temporary files like in case of migrating documents.
+     */
+    @Nullable
+    File getCacheDir(@NonNull Context context);
+
+    /** Returns if we can log INFO level logs. */
+    boolean isInfoLoggingEnabled();
+}
diff --git a/framework/java/external/android/app/appsearch/AppSearchResult.java b/framework/java/external/android/app/appsearch/AppSearchResult.java
index 9a621ec..52c1d6e 100644
--- a/framework/java/external/android/app/appsearch/AppSearchResult.java
+++ b/framework/java/external/android/app/appsearch/AppSearchResult.java
@@ -15,6 +15,7 @@
  */
 package android.app.appsearch;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -22,6 +23,7 @@
 import android.app.appsearch.util.LogUtil;
 import android.util.Log;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.io.IOException;
@@ -55,6 +57,7 @@
                 RESULT_SECURITY_ERROR,
                 RESULT_DENIED,
                 RESULT_RATE_LIMITED,
+                RESULT_TIMED_OUT
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ResultCode {}
@@ -101,22 +104,22 @@
     /**
      * The requested operation is denied for the caller. This error is logged and returned for
      * denylist rejections.
-     *
-     * @hide
      */
-    // TODO(b/279047435): unhide this the next time we can make API changes
+    @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
     public static final int RESULT_DENIED = 9;
 
     /**
-     * The caller has hit AppSearch's rate limit and the requested operation has been rejected.
-     *
-     * @hide
+     * The caller has hit AppSearch's rate limit and the requested operation has been rejected. The
+     * caller is recommended to reschedule tasks with exponential backoff.
      */
-
-    // TODO(b/279047435): unhide this the next time we can make API changes
+    @FlaggedApi(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
     public static final int RESULT_RATE_LIMITED = 10;
 
-    private final @ResultCode int mResultCode;
+    /** The operation was timed out. */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    public static final int RESULT_TIMED_OUT = 11;
+
+    @ResultCode private final int mResultCode;
     @Nullable private final ValueType mResultValue;
     @Nullable private final String mErrorMessage;
 
@@ -206,7 +209,7 @@
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> newSuccessfulResult(
             @Nullable ValueType value) {
-        return new AppSearchResult<>(RESULT_OK, value, /*errorMessage=*/ null);
+        return new AppSearchResult<>(RESULT_OK, value, /* errorMessage= */ null);
     }
 
     /**
@@ -218,7 +221,7 @@
     @NonNull
     public static <ValueType> AppSearchResult<ValueType> newFailedResult(
             @ResultCode int resultCode, @Nullable String errorMessage) {
-        return new AppSearchResult<>(resultCode, /*resultValue=*/ null, errorMessage);
+        return new AppSearchResult<>(resultCode, /* resultValue= */ null, errorMessage);
     }
 
     /**
diff --git a/framework/java/external/android/app/appsearch/AppSearchSchema.java b/framework/java/external/android/app/appsearch/AppSearchSchema.java
index ab1277b..f97ac2f 100644
--- a/framework/java/external/android/app/appsearch/AppSearchSchema.java
+++ b/framework/java/external/android/app/appsearch/AppSearchSchema.java
@@ -24,7 +24,6 @@
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.app.appsearch.exceptions.IllegalSchemaException;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.PropertyConfigParcel;
 import android.app.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
@@ -37,6 +36,7 @@
 import android.os.Parcelable;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -60,6 +60,7 @@
  * @see AppSearchSession#setSchema
  */
 @SafeParcelable.Class(creator = "AppSearchSchemaCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class AppSearchSchema extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -75,14 +76,19 @@
     @Field(id = 3, getter = "getParentTypes")
     private final List<String> mParentTypes;
 
+    @Field(id = 4, getter = "getDescription")
+    private final String mDescription;
+
     @Constructor
     AppSearchSchema(
             @Param(id = 1) @NonNull String schemaType,
             @Param(id = 2) @NonNull List<PropertyConfigParcel> propertyConfigParcels,
-            @Param(id = 3) @NonNull List<String> parentTypes) {
+            @Param(id = 3) @NonNull List<String> parentTypes,
+            @Param(id = 4) @NonNull String description) {
         mSchemaType = Objects.requireNonNull(schemaType);
         mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels);
         mParentTypes = Objects.requireNonNull(parentTypes);
+        mDescription = Objects.requireNonNull(description);
     }
 
     @Override
@@ -105,6 +111,7 @@
         builder.append("{\n");
         builder.increaseIndentLevel();
         builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
+        builder.append("description: \"").append(getDescription()).append("\",\n");
         builder.append("properties: [\n");
 
         AppSearchSchema.PropertyConfig[] sortedProperties =
@@ -134,6 +141,21 @@
     }
 
     /**
+     * Returns a natural language description of this schema type.
+     *
+     * <p>Ex. The description for an Email type could be "A type of electronic message".
+     *
+     * <p>This information is purely to help apps consuming this type to understand its semantic
+     * meaning. This field has no effect in AppSearch - it is just stored with the AppSearchSchema.
+     * If {@link Builder#setDescription} is uncalled, then this method will return an empty string.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+    @NonNull
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
      * Returns the list of {@link PropertyConfig}s that are part of this schema.
      *
      * <p>This method creates a new list when called.
@@ -151,11 +173,8 @@
         return ret;
     }
 
-    /**
-     * Returns the list of parent types of this schema for polymorphism.
-     *
-     * @hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline are possible.
-     */
+    /** Returns the list of parent types of this schema for polymorphism. */
+    @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
     @NonNull
     public List<String> getParentTypes() {
         return Collections.unmodifiableList(mParentTypes);
@@ -173,6 +192,9 @@
         if (!getSchemaType().equals(otherSchema.getSchemaType())) {
             return false;
         }
+        if (!getDescription().equals(otherSchema.getDescription())) {
+            return false;
+        }
         if (!getParentTypes().equals(otherSchema.getParentTypes())) {
             return false;
         }
@@ -181,7 +203,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(getSchemaType(), getProperties(), getParentTypes());
+        return Objects.hash(getSchemaType(), getProperties(), getParentTypes(), getDescription());
     }
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -193,6 +215,7 @@
     /** Builder for {@link AppSearchSchema objects}. */
     public static final class Builder {
         private final String mSchemaType;
+        private String mDescription = "";
         private ArrayList<PropertyConfigParcel> mPropertyConfigParcels = new ArrayList<>();
         private LinkedHashSet<String> mParentTypes = new LinkedHashSet<>();
         private final Set<String> mPropertyNames = new ArraySet<>();
@@ -203,6 +226,22 @@
             mSchemaType = Objects.requireNonNull(schemaType);
         }
 
+        /**
+         * Sets a natural language description of this schema type.
+         *
+         * <p>For more details about the description field, see {@link
+         * AppSearchSchema#getDescription}.
+         */
+        @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+        @CanIgnoreReturnValue
+        @NonNull
+        public AppSearchSchema.Builder setDescription(@NonNull String description) {
+            Objects.requireNonNull(description);
+            resetIfBuilt();
+            mDescription = description;
+            return this;
+        }
+
         /** Adds a property to the given type. */
         @CanIgnoreReturnValue
         @NonNull
@@ -291,7 +330,10 @@
         public AppSearchSchema build() {
             mBuilt = true;
             return new AppSearchSchema(
-                    mSchemaType, mPropertyConfigParcels, new ArrayList<>(mParentTypes));
+                    mSchemaType,
+                    mPropertyConfigParcels,
+                    new ArrayList<>(mParentTypes),
+                    mDescription);
         }
 
         private void resetIfBuilt() {
@@ -326,20 +368,37 @@
                     DATA_TYPE_BOOLEAN,
                     DATA_TYPE_BYTES,
                     DATA_TYPE_DOCUMENT,
+                    DATA_TYPE_EMBEDDING,
                 })
         @Retention(RetentionPolicy.SOURCE)
         public @interface DataType {}
 
-        /** @hide */
+        /**
+         * Constant value for String data type.
+         *
+         * @hide
+         */
         public static final int DATA_TYPE_STRING = 1;
 
-        /** @hide */
+        /**
+         * Constant value for Long data type.
+         *
+         * @hide
+         */
         public static final int DATA_TYPE_LONG = 2;
 
-        /** @hide */
+        /**
+         * Constant value for Double data type.
+         *
+         * @hide
+         */
         public static final int DATA_TYPE_DOUBLE = 3;
 
-        /** @hide */
+        /**
+         * Constant value for Boolean data type.
+         *
+         * @hide
+         */
         public static final int DATA_TYPE_BOOLEAN = 4;
 
         /**
@@ -359,6 +418,13 @@
         public static final int DATA_TYPE_DOCUMENT = 6;
 
         /**
+         * Indicates that the property is an {@link EmbeddingVector}.
+         *
+         * @hide
+         */
+        public static final int DATA_TYPE_EMBEDDING = 7;
+
+        /**
          * The cardinality of the property (whether it is required, optional or repeated).
          *
          * <p>NOTE: The integer values of these constants must match the proto enum constants in
@@ -410,6 +476,7 @@
             builder.append("{\n");
             builder.increaseIndentLevel();
             builder.append("name: \"").append(getName()).append("\",\n");
+            builder.append("description: \"").append(getDescription()).append("\",\n");
 
             if (this instanceof AppSearchSchema.StringPropertyConfig) {
                 ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder);
@@ -452,6 +519,9 @@
                 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
                     builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
                     break;
+                case PropertyConfig.DATA_TYPE_EMBEDDING:
+                    builder.append("dataType: DATA_TYPE_EMBEDDING,\n");
+                    break;
                 default:
                     builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
             }
@@ -466,6 +536,23 @@
         }
 
         /**
+         * Returns a natural language description of this property.
+         *
+         * <p>Ex. The description for the "homeAddress" property of a "Person" type could be "the
+         * address at which this person lives".
+         *
+         * <p>This information is purely to help apps consuming this type the semantic meaning of
+         * its properties. This field has no effect in AppSearch - it is just stored with the
+         * AppSearchSchema. If the description is not set, then this method will return an empty
+         * string.
+         */
+        @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+        @NonNull
+        public String getDescription() {
+            return mPropertyConfigParcel.getDescription();
+        }
+
+        /**
          * Returns the type of data the property contains (such as string, int, bytes, etc).
          *
          * @hide
@@ -527,6 +614,8 @@
                     return new BytesPropertyConfig(propertyConfigParcel);
                 case PropertyConfig.DATA_TYPE_DOCUMENT:
                     return new DocumentPropertyConfig(propertyConfigParcel);
+                case PropertyConfig.DATA_TYPE_EMBEDDING:
+                    return new EmbeddingPropertyConfig(propertyConfigParcel);
                 default:
                     throw new IllegalArgumentException(
                             "Unsupported property bundle of type "
@@ -676,15 +765,27 @@
         }
 
         /** Returns how the property is indexed. */
-        @IndexingType
+        @StringPropertyConfig.IndexingType
         public int getIndexingType() {
-            return mPropertyConfigParcel.getStringIndexingConfigParcel().getIndexingType();
+            StringIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getStringIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return INDEXING_TYPE_NONE;
+            }
+
+            return indexingConfigParcel.getIndexingType();
         }
 
         /** Returns how this property is tokenized (split into words). */
         @TokenizerType
         public int getTokenizerType() {
-            return mPropertyConfigParcel.getStringIndexingConfigParcel().getTokenizerType();
+            StringIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getStringIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return TOKENIZER_TYPE_NONE;
+            }
+
+            return indexingConfigParcel.getTokenizerType();
         }
 
         /**
@@ -692,24 +793,21 @@
          */
         @JoinableValueType
         public int getJoinableValueType() {
-            return mPropertyConfigParcel.getJoinableConfigParcel().getJoinableValueType();
-        }
+            JoinableConfigParcel joinableConfigParcel =
+                    mPropertyConfigParcel.getJoinableConfigParcel();
+            if (joinableConfigParcel == null) {
+                return JOINABLE_VALUE_TYPE_NONE;
+            }
 
-        /**
-         * Returns whether or not documents in this schema should be deleted when the document
-         * referenced by this field is deleted.
-         *
-         * @hide
-         */
-        public boolean getDeletionPropagation() {
-            return mPropertyConfigParcel.getJoinableConfigParcel().getDeletionPropagation();
+            return joinableConfigParcel.getJoinableValueType();
         }
 
         /** Builder for {@link StringPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
+            private String mDescription = "";
             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
-            @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+            @StringPropertyConfig.IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
             @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE;
             @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE;
             private boolean mDeletionPropagation = false;
@@ -720,6 +818,21 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public StringPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is {@link
@@ -743,7 +856,8 @@
              */
             @CanIgnoreReturnValue
             @NonNull
-            public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+            public StringPropertyConfig.Builder setIndexingType(
+                    @StringPropertyConfig.IndexingType int indexingType) {
                 Preconditions.checkArgumentInRange(
                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
                 mIndexingType = indexingType;
@@ -791,21 +905,6 @@
                 return this;
             }
 
-            /**
-             * Configures whether or not documents in this schema will be removed when the document
-             * referred to by this property is deleted.
-             *
-             * <p>Requires that a joinable value type is set.
-             *
-             * @hide
-             */
-            @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation
-            @NonNull
-            public Builder setDeletionPropagation(boolean deletionPropagation) {
-                mDeletionPropagation = deletionPropagation;
-                return this;
-            }
-
             /** Constructs a new {@link StringPropertyConfig} from the contents of this builder. */
             @NonNull
             public StringPropertyConfig build() {
@@ -824,7 +923,7 @@
                     Preconditions.checkState(
                             mCardinality != CARDINALITY_REPEATED,
                             "Cannot set JOINABLE_VALUE_TYPE_QUALIFIED_ID with"
-                                + " CARDINALITY_REPEATED.");
+                                    + " CARDINALITY_REPEATED.");
                 } else {
                     Preconditions.checkState(
                             !mDeletionPropagation,
@@ -838,6 +937,7 @@
                 return new StringPropertyConfig(
                         PropertyConfigParcel.createForString(
                                 mPropertyName,
+                                mDescription,
                                 mCardinality,
                                 stringConfigParcel,
                                 joinableConfigParcel));
@@ -925,16 +1025,22 @@
         }
 
         /** Returns how the property is indexed. */
-        @IndexingType
+        @LongPropertyConfig.IndexingType
         public int getIndexingType() {
-            return mPropertyConfigParcel.getIntegerIndexingConfigParcel().getIndexingType();
+            PropertyConfigParcel.IntegerIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getIntegerIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return INDEXING_TYPE_NONE;
+            }
+            return indexingConfigParcel.getIndexingType();
         }
 
         /** Builder for {@link LongPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
+            private String mDescription = "";
             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
-            @IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+            @LongPropertyConfig.IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
 
             /** Creates a new {@link LongPropertyConfig.Builder}. */
             public Builder(@NonNull String propertyName) {
@@ -942,6 +1048,21 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public LongPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is {@link
@@ -966,7 +1087,8 @@
              */
             @CanIgnoreReturnValue
             @NonNull
-            public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
+            public LongPropertyConfig.Builder setIndexingType(
+                    @LongPropertyConfig.IndexingType int indexingType) {
                 Preconditions.checkArgumentInRange(
                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType");
                 mIndexingType = indexingType;
@@ -978,7 +1100,7 @@
             public LongPropertyConfig build() {
                 return new LongPropertyConfig(
                         PropertyConfigParcel.createForLong(
-                                mPropertyName, mCardinality, mIndexingType));
+                                mPropertyName, mDescription, mCardinality, mIndexingType));
             }
         }
 
@@ -1013,6 +1135,7 @@
         /** Builder for {@link DoublePropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
+            private String mDescription = "";
             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
 
             /** Creates a new {@link DoublePropertyConfig.Builder}. */
@@ -1021,6 +1144,21 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public DoublePropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is {@link
@@ -1040,7 +1178,8 @@
             @NonNull
             public DoublePropertyConfig build() {
                 return new DoublePropertyConfig(
-                        PropertyConfigParcel.createForDouble(mPropertyName, mCardinality));
+                        PropertyConfigParcel.createForDouble(
+                                mPropertyName, mDescription, mCardinality));
             }
         }
     }
@@ -1054,6 +1193,7 @@
         /** Builder for {@link BooleanPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
+            private String mDescription = "";
             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
 
             /** Creates a new {@link BooleanPropertyConfig.Builder}. */
@@ -1062,6 +1202,21 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public BooleanPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is {@link
@@ -1081,7 +1236,8 @@
             @NonNull
             public BooleanPropertyConfig build() {
                 return new BooleanPropertyConfig(
-                        PropertyConfigParcel.createForBoolean(mPropertyName, mCardinality));
+                        PropertyConfigParcel.createForBoolean(
+                                mPropertyName, mDescription, mCardinality));
             }
         }
     }
@@ -1095,6 +1251,7 @@
         /** Builder for {@link BytesPropertyConfig}. */
         public static final class Builder {
             private final String mPropertyName;
+            private String mDescription = "";
             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
 
             /** Creates a new {@link BytesPropertyConfig.Builder}. */
@@ -1103,6 +1260,21 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public BytesPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is {@link
@@ -1122,7 +1294,8 @@
             @NonNull
             public BytesPropertyConfig build() {
                 return new BytesPropertyConfig(
-                        PropertyConfigParcel.createForBytes(mPropertyName, mCardinality));
+                        PropertyConfigParcel.createForBytes(
+                                mPropertyName, mDescription, mCardinality));
             }
         }
     }
@@ -1150,25 +1323,31 @@
          *     a subset of properties from the nested document.
          */
         public boolean shouldIndexNestedProperties() {
-            return mPropertyConfigParcel
-                    .getDocumentIndexingConfigParcel()
-                    .shouldIndexNestedProperties();
+            DocumentIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getDocumentIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return false;
+            }
+
+            return indexingConfigParcel.shouldIndexNestedProperties();
         }
 
-        /**
-         * Returns the list of indexable nested properties for the nested document.
-         *
-         * @hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline are possible.
-         */
+        /** Returns the list of indexable nested properties for the nested document. */
+        @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
         @NonNull
         public List<String> getIndexableNestedProperties() {
+            DocumentIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getDocumentIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return Collections.emptyList();
+            }
+
             List<String> indexableNestedPropertiesList =
-                    mPropertyConfigParcel
-                            .getDocumentIndexingConfigParcel()
-                            .getIndexableNestedPropertiesList();
+                    indexingConfigParcel.getIndexableNestedPropertiesList();
             if (indexableNestedPropertiesList == null) {
                 return Collections.emptyList();
             }
+
             return Collections.unmodifiableList(indexableNestedPropertiesList);
         }
 
@@ -1176,6 +1355,7 @@
         public static final class Builder {
             private final String mPropertyName;
             private final String mSchemaType;
+            private String mDescription = "";
             @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
             private boolean mShouldIndexNestedProperties = false;
             private final Set<String> mIndexableNestedPropertiesList = new ArraySet<>();
@@ -1195,6 +1375,21 @@
             }
 
             /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public DocumentPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
              * Sets the cardinality of the property (whether it is optional, required or repeated).
              *
              * <p>If this method is not called, the default cardinality is {@link
@@ -1232,9 +1427,8 @@
              * Adds one or more properties for indexing from the nested document property.
              *
              * @see #addIndexableNestedProperties(Collection)
-             * @hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline are
-             *     possible.
              */
+            @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
             @CanIgnoreReturnValue
             @NonNull
             public DocumentPropertyConfig.Builder addIndexableNestedProperties(
@@ -1247,9 +1441,8 @@
              * Adds one or more property paths for indexing from the nested document property.
              *
              * @see #addIndexableNestedProperties(Collection)
-             * @hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline are
-             *     possible.
              */
+            @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
             @CanIgnoreReturnValue
             @SuppressLint("MissingGetterMatchingBuilder")
             @NonNull
@@ -1302,9 +1495,8 @@
              * Adds one or more property paths for indexing from the nested document property.
              *
              * @see #addIndexableNestedProperties(Collection)
-             * @hide TODO(b/291122592): Unhide in Mainline when API updates via Mainline are
-             *     possible.
              */
+            @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
             @CanIgnoreReturnValue
             @SuppressLint("MissingGetterMatchingBuilder")
             @NonNull
@@ -1336,6 +1528,7 @@
                 return new DocumentPropertyConfig(
                         PropertyConfigParcel.createForDocument(
                                 mPropertyName,
+                                mDescription,
                                 mCardinality,
                                 mSchemaType,
                                 new DocumentIndexingConfigParcel(
@@ -1364,4 +1557,116 @@
             builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
         }
     }
+
+    /** Configuration for a property of type {@link EmbeddingVector} in a Document. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final class EmbeddingPropertyConfig extends PropertyConfig {
+        /**
+         * Encapsulates the configurations on how AppSearch should query/index these embedding
+         * vectors.
+         *
+         * @hide
+         */
+        @IntDef(value = {INDEXING_TYPE_NONE, INDEXING_TYPE_SIMILARITY})
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface IndexingType {}
+
+        /** Content in this property will not be indexed. */
+        public static final int INDEXING_TYPE_NONE = 0;
+
+        /**
+         * Embedding vectors in this property will be indexed.
+         *
+         * <p>The index offers 100% accuracy, but has linear time complexity based on the number of
+         * embedding vectors within the index.
+         */
+        public static final int INDEXING_TYPE_SIMILARITY = 1;
+
+        EmbeddingPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) {
+            super(propertyConfigParcel);
+        }
+
+        /** Returns how the property is indexed. */
+        @EmbeddingPropertyConfig.IndexingType
+        public int getIndexingType() {
+            PropertyConfigParcel.EmbeddingIndexingConfigParcel indexingConfigParcel =
+                    mPropertyConfigParcel.getEmbeddingIndexingConfigParcel();
+            if (indexingConfigParcel == null) {
+                return INDEXING_TYPE_NONE;
+            }
+            return indexingConfigParcel.getIndexingType();
+        }
+
+        /** Builder for {@link EmbeddingPropertyConfig}. */
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public static final class Builder {
+            private final String mPropertyName;
+            private String mDescription = "";
+            @Cardinality private int mCardinality = CARDINALITY_OPTIONAL;
+            @EmbeddingPropertyConfig.IndexingType private int mIndexingType = INDEXING_TYPE_NONE;
+
+            /** Creates a new {@link EmbeddingPropertyConfig.Builder}. */
+            public Builder(@NonNull String propertyName) {
+                mPropertyName = Objects.requireNonNull(propertyName);
+            }
+
+            /**
+             * Sets a natural language description of this property.
+             *
+             * <p>For more details about the description field, see {@link
+             * AppSearchSchema.PropertyConfig#getDescription}.
+             */
+            @CanIgnoreReturnValue
+            @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS)
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public EmbeddingPropertyConfig.Builder setDescription(@NonNull String description) {
+                mDescription = Objects.requireNonNull(description);
+                return this;
+            }
+
+            /**
+             * Sets the cardinality of the property (whether it is optional, required or repeated).
+             *
+             * <p>If this method is not called, the default cardinality is {@link
+             * PropertyConfig#CARDINALITY_OPTIONAL}.
+             */
+            @CanIgnoreReturnValue
+            @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
+            @NonNull
+            public EmbeddingPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
+                Preconditions.checkArgumentInRange(
+                        cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
+                mCardinality = cardinality;
+                return this;
+            }
+
+            /**
+             * Configures how a property should be indexed so that it can be retrieved by queries.
+             *
+             * <p>If this method is not called, the default indexing type is {@link
+             * EmbeddingPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed and
+             * cannot be matched by queries.
+             */
+            @CanIgnoreReturnValue
+            @NonNull
+            public EmbeddingPropertyConfig.Builder setIndexingType(
+                    @EmbeddingPropertyConfig.IndexingType int indexingType) {
+                Preconditions.checkArgumentInRange(
+                        indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_SIMILARITY, "indexingType");
+                mIndexingType = indexingType;
+                return this;
+            }
+
+            /**
+             * Constructs a new {@link EmbeddingPropertyConfig} from the contents of this builder.
+             */
+            @NonNull
+            public EmbeddingPropertyConfig build() {
+                return new EmbeddingPropertyConfig(
+                        PropertyConfigParcel.createForEmbedding(
+                                mPropertyName, mDescription, mCardinality, mIndexingType));
+            }
+        }
+    }
 }
diff --git a/framework/java/external/android/app/appsearch/EmbeddingVector.java b/framework/java/external/android/app/appsearch/EmbeddingVector.java
new file mode 100644
index 0000000..4b6028b
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/EmbeddingVector.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.appsearch.flags.Flags;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Embeddings are vector representations of data, such as text, images, and audio, which can be
+ * generated by machine learning models and used for semantic search. This class represents an
+ * embedding vector, which wraps a float array for the values of the embedding vector and a model
+ * signature that can be any string to distinguish between embedding vectors generated by different
+ * models.
+ *
+ * <p>For more details on how embedding search works, check {@link AppSearchSession#search} and
+ * {@link SearchSpec.Builder#setRankingStrategy(String)}.
+ *
+ * @see SearchSpec.Builder#addSearchEmbeddings
+ * @see GenericDocument.Builder#setPropertyEmbedding
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
[email protected](creator = "EmbeddingVectorCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class EmbeddingVector extends AbstractSafeParcelable {
+
+    @NonNull
+    public static final Parcelable.Creator<EmbeddingVector> CREATOR = new EmbeddingVectorCreator();
+
+    @NonNull
+    @Field(id = 1, getter = "getValues")
+    private final float[] mValues;
+
+    @NonNull
+    @Field(id = 2, getter = "getModelSignature")
+    private final String mModelSignature;
+
+    @Nullable private Integer mHashCode;
+
+    /**
+     * Creates a new {@link EmbeddingVector}.
+     *
+     * @throws IllegalArgumentException if {@code values} is empty.
+     */
+    @Constructor
+    public EmbeddingVector(
+            @Param(id = 1) @NonNull float[] values, @Param(id = 2) @NonNull String modelSignature) {
+        mValues = Objects.requireNonNull(values);
+        if (mValues.length == 0) {
+            throw new IllegalArgumentException("Embedding values cannot be empty.");
+        }
+        mModelSignature = Objects.requireNonNull(modelSignature);
+    }
+
+    /** Returns the values of this embedding vector. */
+    @NonNull
+    public float[] getValues() {
+        return mValues;
+    }
+
+    /**
+     * Returns the model signature of this embedding vector, which is an arbitrary string to
+     * distinguish between embedding vectors generated by different models.
+     */
+    @NonNull
+    public String getModelSignature() {
+        return mModelSignature;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null) return false;
+        if (!(o instanceof EmbeddingVector)) return false;
+        EmbeddingVector that = (EmbeddingVector) o;
+        return Arrays.equals(mValues, that.mValues) && mModelSignature.equals(that.mModelSignature);
+    }
+
+    @Override
+    public int hashCode() {
+        if (mHashCode == null) {
+            mHashCode = Objects.hash(Arrays.hashCode(mValues), mModelSignature);
+        }
+        return mHashCode;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        EmbeddingVectorCreator.writeToParcel(this, dest, flags);
+    }
+}
diff --git a/framework/java/external/android/app/appsearch/FeatureConstants.java b/framework/java/external/android/app/appsearch/FeatureConstants.java
index b440075..feb21c9 100644
--- a/framework/java/external/android/app/appsearch/FeatureConstants.java
+++ b/framework/java/external/android/app/appsearch/FeatureConstants.java
@@ -16,7 +16,6 @@
 
 package android.app.appsearch;
 
-
 /**
  * A class that encapsulates all feature constants that are accessible in AppSearch framework.
  *
@@ -26,16 +25,25 @@
  * @see Features
  * @hide
  */
-public interface FeatureConstants {
+public final class FeatureConstants {
     /** Feature constants for {@link Features#NUMERIC_SEARCH}. */
-    String NUMERIC_SEARCH = "NUMERIC_SEARCH";
+    public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
 
     /** Feature constants for {@link Features#VERBATIM_SEARCH}. */
-    String VERBATIM_SEARCH = "VERBATIM_SEARCH";
+    public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
 
     /** Feature constants for {@link Features#LIST_FILTER_QUERY_LANGUAGE}. */
-    String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+    public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
 
     /** Feature constants for {@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}. */
-    String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
+    public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION =
+            "LIST_FILTER_HAS_PROPERTY_FUNCTION";
+
+    /** A feature constant for the "semanticSearch" function in {@link AppSearchSession#search}. */
+    public static final String EMBEDDING_SEARCH = "EMBEDDING_SEARCH";
+
+    /** A feature constant for the "tokenize" function in {@link AppSearchSession#search}. */
+    public static final String LIST_FILTER_TOKENIZE_FUNCTION = "TOKENIZE";
+
+    private FeatureConstants() {}
 }
diff --git a/framework/java/external/android/app/appsearch/GenericDocument.java b/framework/java/external/android/app/appsearch/GenericDocument.java
index b292c1a..ebae4c6 100644
--- a/framework/java/external/android/app/appsearch/GenericDocument.java
+++ b/framework/java/external/android/app/appsearch/GenericDocument.java
@@ -23,12 +23,13 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.safeparcel.PropertyParcel;
 import android.app.appsearch.util.IndentingStringBuilder;
 import android.util.Log;
 
+import com.android.appsearch.flags.Flags;
+
 import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -56,10 +57,21 @@
     /** The maximum number of indexed properties a document can have. */
     private static final int MAX_INDEXED_PROPERTIES = 16;
 
-    /** @hide */
+    /**
+     * Fixed constant synthetic property for parent types.
+     *
+     * @hide
+     */
     public static final String PARENT_TYPES_SYNTHETIC_PROPERTY = "$$__AppSearch__parentTypes";
 
     /**
+     * An immutable empty {@link GenericDocument}.
+     *
+     * @hide
+     */
+    public static final GenericDocument EMPTY = new GenericDocument.Builder<>("", "", "").build();
+
+    /**
      * The maximum number of indexed properties a document can have.
      *
      * <p>Indexed properties are properties which are strings where the {@link
@@ -252,7 +264,9 @@
         Objects.requireNonNull(path);
         Object rawValue =
                 getRawPropertyFromRawDocument(
-                        new PropertyPath(path), /*pathIndex=*/ 0, mDocumentParcel.getPropertyMap());
+                        new PropertyPath(path),
+                        /* pathIndex= */ 0,
+                        mDocumentParcel.getPropertyMap());
 
         // Unpack the raw value into the types the user expects, if required.
         if (rawValue instanceof GenericDocumentParcel) {
@@ -325,36 +339,41 @@
                 Object extractedValue = null;
                 if (propertyParcel.getStringValues() != null) {
                     String[] stringValues = propertyParcel.getStringValues();
-                    if (index < stringValues.length) {
+                    if (stringValues != null && index < stringValues.length) {
                         extractedValue = Arrays.copyOfRange(stringValues, index, index + 1);
                     }
                 } else if (propertyParcel.getLongValues() != null) {
                     long[] longValues = propertyParcel.getLongValues();
-                    if (index < longValues.length) {
+                    if (longValues != null && index < longValues.length) {
                         extractedValue = Arrays.copyOfRange(longValues, index, index + 1);
                     }
                 } else if (propertyParcel.getDoubleValues() != null) {
                     double[] doubleValues = propertyParcel.getDoubleValues();
-                    if (index < doubleValues.length) {
+                    if (doubleValues != null && index < doubleValues.length) {
                         extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1);
                     }
                 } else if (propertyParcel.getBooleanValues() != null) {
                     boolean[] booleanValues = propertyParcel.getBooleanValues();
-                    if (index < booleanValues.length) {
+                    if (booleanValues != null && index < booleanValues.length) {
                         extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1);
                     }
                 } else if (propertyParcel.getBytesValues() != null) {
                     byte[][] bytesValues = propertyParcel.getBytesValues();
-                    if (index < bytesValues.length) {
+                    if (bytesValues != null && index < bytesValues.length) {
                         extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1);
                     }
                 } else if (propertyParcel.getDocumentValues() != null) {
                     // Special optimization: to avoid creating new singleton arrays for traversing
                     // paths we return the bare document parcel in this particular case.
                     GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues();
-                    if (index < docValues.length) {
+                    if (docValues != null && index < docValues.length) {
                         extractedValue = docValues[index];
                     }
+                } else if (propertyParcel.getEmbeddingValues() != null) {
+                    EmbeddingVector[] embeddingValues = propertyParcel.getEmbeddingValues();
+                    if (embeddingValues != null && index < embeddingValues.length) {
+                        extractedValue = Arrays.copyOfRange(embeddingValues, index, index + 1);
+                    }
                 } else {
                     throw new IllegalStateException(
                             "Unsupported value type: " + currentElementValue);
@@ -381,7 +400,7 @@
                     && ((PropertyParcel) currentElementValue).getDocumentValues() != null) {
                 GenericDocumentParcel[] docParcels =
                         ((PropertyParcel) currentElementValue).getDocumentValues();
-                if (docParcels.length == 1) {
+                if (docParcels != null && docParcels.length == 1) {
                     propertyMap = docParcels[0].getPropertyMap();
                     continue;
                 }
@@ -409,20 +428,22 @@
                 // repeated values. The implementation is optimized for these two cases, requiring
                 // no additional allocations. So we've decided that the above performance
                 // characteristics are OK for the less used path.
-                List<Object> accumulator = new ArrayList<>(docParcels.length);
-                for (GenericDocumentParcel docParcel : docParcels) {
-                    // recurse as we need to branch
-                    Object value =
-                            getRawPropertyFromRawDocument(
-                                    path,
-                                    /*pathIndex=*/ i + 1,
-                                    ((GenericDocumentParcel) docParcel).getPropertyMap());
-                    if (value != null) {
-                        accumulator.add(value);
+                if (docParcels != null) {
+                    List<Object> accumulator = new ArrayList<>(docParcels.length);
+                    for (GenericDocumentParcel docParcel : docParcels) {
+                        // recurse as we need to branch
+                        Object value =
+                                getRawPropertyFromRawDocument(
+                                        path,
+                                        /* pathIndex= */ i + 1,
+                                        ((GenericDocumentParcel) docParcel).getPropertyMap());
+                        if (value != null) {
+                            accumulator.add(value);
+                        }
                     }
+                    // Break the path traversing loop
+                    return flattenAccumulator(accumulator);
                 }
-                // Break the path traversing loop
-                return flattenAccumulator(accumulator);
             } else {
                 Log.e(TAG, "Failed to apply path to document; no nested value found: " + path);
                 return null;
@@ -651,6 +672,27 @@
         return propertyArray[0];
     }
 
+    /**
+     * Retrieves an {@code EmbeddingVector} property by path.
+     *
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * @param path The path to look for.
+     * @return The first {@code EmbeddingVector[]} associated with the given path or {@code null} if
+     *     there is no such value or the value is of a different type.
+     */
+    @Nullable
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public EmbeddingVector getPropertyEmbedding(@NonNull String path) {
+        Objects.requireNonNull(path);
+        EmbeddingVector[] propertyArray = getPropertyEmbeddingArray(path);
+        if (propertyArray == null || propertyArray.length == 0) {
+            return null;
+        }
+        warnIfSinglePropertyTooLong("Embedding", path, propertyArray.length);
+        return propertyArray[0];
+    }
+
     /** Prints a warning to logcat if the given propertyLength is greater than 1. */
     private static void warnIfSinglePropertyTooLong(
             @NonNull String propertyType, @NonNull String path, int propertyLength) {
@@ -809,6 +851,30 @@
     }
 
     /**
+     * Retrieves a repeated {@code EmbeddingVector[]} property by path.
+     *
+     * <p>See {@link #getProperty} for a detailed description of the path syntax.
+     *
+     * <p>If the property has not been set via {@link Builder#setPropertyEmbedding}, this method
+     * returns {@code null}.
+     *
+     * <p>If it has been set via {@link Builder#setPropertyEmbedding} to an empty {@code
+     * EmbeddingVector[]}, this method returns an empty {@code EmbeddingVector[]}.
+     *
+     * @param path The path to look for.
+     * @return The {@code EmbeddingVector[]} associated with the given path, or {@code null} if no
+     *     value is set or the value is of a different type.
+     */
+    @SuppressLint({"ArrayReturn", "NullableCollection"})
+    @Nullable
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public EmbeddingVector[] getPropertyEmbeddingArray(@NonNull String path) {
+        Objects.requireNonNull(path);
+        Object value = getProperty(path);
+        return safeCastProperty(path, value, EmbeddingVector[].class);
+    }
+
+    /**
      * Casts a repeated property to the provided type, logging an error and returning {@code null}
      * if the cast fails.
      *
@@ -955,7 +1021,7 @@
                     builder.append("\"").append((String) propertyElement).append("\"");
                 } else if (propertyElement instanceof byte[]) {
                     builder.append(Arrays.toString((byte[]) propertyElement));
-                } else {
+                } else if (propertyElement != null) {
                     builder.append(propertyElement.toString());
                 }
                 if (i != propertyArrLength - 1) {
@@ -974,8 +1040,9 @@
     // This builder is specifically designed to be extended by classes deriving from
     // GenericDocument.
     @SuppressLint("StaticFinalBuilder")
+    @SuppressWarnings("rawtypes")
     public static class Builder<BuilderType extends Builder> {
-        private GenericDocumentParcel.Builder mDocumentParcelBuilder;
+        private final GenericDocumentParcel.Builder mDocumentParcelBuilder;
         private final BuilderType mBuilderTypeInstance;
 
         /**
@@ -1019,8 +1086,8 @@
         /**
          * Creates a new {@link GenericDocument.Builder} from the given GenericDocument.
          *
-         * <p>The GenericDocument is deep copied, i.e. changes to the new GenericDocument returned
-         * by this function will NOT affect the original GenericDocument.
+         * <p>The GenericDocument is deep copied, that is, it changes to a new GenericDocument
+         * returned by this function and will NOT affect the original GenericDocument.
          */
         @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR)
         public Builder(@NonNull GenericDocument document) {
@@ -1289,6 +1356,32 @@
         }
 
         /**
+         * Sets one or multiple {@code EmbeddingVector} values for a property, replacing its
+         * previous values.
+         *
+         * @param name the name associated with the {@code values}. Must match the name for this
+         *     property as given in {@link AppSearchSchema.PropertyConfig#getName}.
+         * @param values the {@code EmbeddingVector} values of the property.
+         * @throws IllegalArgumentException if the name is empty or {@code null}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public BuilderType setPropertyEmbedding(
+                @NonNull String name, @NonNull EmbeddingVector... values) {
+            Objects.requireNonNull(name);
+            Objects.requireNonNull(values);
+            validatePropertyName(name);
+            for (int i = 0; i < values.length; i++) {
+                if (values[i] == null) {
+                    throw new IllegalArgumentException("The EmbeddingVector at " + i + " is null.");
+                }
+            }
+            mDocumentParcelBuilder.putInPropertyMap(name, values);
+            return mBuilderTypeInstance;
+        }
+
+        /**
          * Clears the value for the property with the given name.
          *
          * <p>Note that this method does not support property paths.
diff --git a/framework/java/external/android/app/appsearch/GetByDocumentIdRequest.java b/framework/java/external/android/app/appsearch/GetByDocumentIdRequest.java
index b423e67..33758fe 100644
--- a/framework/java/external/android/app/appsearch/GetByDocumentIdRequest.java
+++ b/framework/java/external/android/app/appsearch/GetByDocumentIdRequest.java
@@ -16,11 +16,21 @@
 
 package android.app.appsearch;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.app.appsearch.util.BundleUtil;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -36,7 +46,15 @@
  *
  * @see AppSearchSession#getByDocumentId
  */
-public final class GetByDocumentIdRequest {
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "GetByDocumentIdRequestCreator")
+public final class GetByDocumentIdRequest extends AbstractSafeParcelable {
+
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<GetByDocumentIdRequest> CREATOR =
+            new GetByDocumentIdRequestCreator();
+
     /**
      * Schema type to be used in {@link GetByDocumentIdRequest.Builder#addProjection} to apply
      * property paths to all results, excepting any types that have had their own, specific property
@@ -44,17 +62,29 @@
      */
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
 
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
     private final String mNamespace;
-    private final Set<String> mIds;
-    private final Map<String, List<String>> mTypePropertyPathsMap;
 
+    @NonNull
+    @Field(id = 2)
+    final List<String> mIds;
+
+    @NonNull
+    @Field(id = 3)
+    final Bundle mTypePropertyPaths;
+
+    /** Cache of the ids. Comes from inflating mIds at first use. */
+    @Nullable private Set<String> mIdsCached;
+
+    @Constructor
     GetByDocumentIdRequest(
-            @NonNull String namespace,
-            @NonNull Set<String> ids,
-            @NonNull Map<String, List<String>> typePropertyPathsMap) {
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull List<String> ids,
+            @Param(id = 3) @NonNull Bundle typePropertyPaths) {
         mNamespace = Objects.requireNonNull(namespace);
         mIds = Objects.requireNonNull(ids);
-        mTypePropertyPathsMap = Objects.requireNonNull(typePropertyPathsMap);
+        mTypePropertyPaths = Objects.requireNonNull(typePropertyPaths);
     }
 
     /** Returns the namespace attached to the request. */
@@ -66,7 +96,10 @@
     /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set<String> getIds() {
-        return Collections.unmodifiableSet(mIds);
+        if (mIdsCached == null) {
+            mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+        }
+        return mIdsCached;
     }
 
     /**
@@ -79,11 +112,15 @@
      */
     @NonNull
     public Map<String, List<String>> getProjections() {
-        Map<String, List<String>> copy = new ArrayMap<>();
-        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
-            copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        Set<String> schemas = mTypePropertyPaths.keySet();
+        Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            List<String> propertyPaths = mTypePropertyPaths.getStringArrayList(schema);
+            if (propertyPaths != null) {
+                typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPaths));
+            }
         }
-        return copy;
+        return typePropertyPathsMap;
     }
 
     /**
@@ -96,37 +133,33 @@
      */
     @NonNull
     public Map<String, List<PropertyPath>> getProjectionPaths() {
-        Map<String, List<PropertyPath>> copy = new ArrayMap<>(mTypePropertyPathsMap.size());
-        for (Map.Entry<String, List<String>> entry : mTypePropertyPathsMap.entrySet()) {
-            List<PropertyPath> propertyPathList = new ArrayList<>(entry.getValue().size());
-            for (String p : entry.getValue()) {
-                propertyPathList.add(new PropertyPath(p));
+        Set<String> schemas = mTypePropertyPaths.keySet();
+        Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            List<String> paths = mTypePropertyPaths.getStringArrayList(schema);
+            if (paths != null) {
+                int pathsSize = paths.size();
+                List<PropertyPath> propertyPathList = new ArrayList<>(pathsSize);
+                for (int i = 0; i < pathsSize; i++) {
+                    propertyPathList.add(new PropertyPath(paths.get(i)));
+                }
+                typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPathList));
             }
-            copy.put(entry.getKey(), propertyPathList);
         }
-        return copy;
+        return typePropertyPathsMap;
     }
 
-    /**
-     * Returns a map from schema type to property paths to be used for projection.
-     *
-     * <p>If the map is empty, then all properties will be retrieved for all results.
-     *
-     * <p>A more efficient version of {@link #getProjections}, but it returns a modifiable map. This
-     * is not meant to be unhidden and should only be used by internal classes.
-     *
-     * @hide
-     */
-    @NonNull
-    public Map<String, List<String>> getProjectionsInternal() {
-        return mTypePropertyPathsMap;
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        GetByDocumentIdRequestCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link GetByDocumentIdRequest} objects. */
     public static final class Builder {
         private final String mNamespace;
-        private ArraySet<String> mIds = new ArraySet<>();
-        private ArrayMap<String, List<String>> mProjectionTypePropertyPaths = new ArrayMap<>();
+        private List<String> mIds = new ArrayList<>();
+        private Bundle mProjectionTypePropertyPaths = new Bundle();
         private boolean mBuilt = false;
 
         /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */
@@ -176,12 +209,12 @@
             Objects.requireNonNull(schemaType);
             Objects.requireNonNull(propertyPaths);
             resetIfBuilt();
-            List<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
+            ArrayList<String> propertyPathsList = new ArrayList<>(propertyPaths.size());
             for (String propertyPath : propertyPaths) {
                 Objects.requireNonNull(propertyPath);
                 propertyPathsList.add(propertyPath);
             }
-            mProjectionTypePropertyPaths.put(schemaType, propertyPathsList);
+            mProjectionTypePropertyPaths.putStringArrayList(schemaType, propertyPathsList);
             return this;
         }
 
@@ -223,11 +256,11 @@
 
         private void resetIfBuilt() {
             if (mBuilt) {
-                mIds = new ArraySet<>(mIds);
+                mIds = new ArrayList<>(mIds);
                 // No need to clone each propertyPathsList inside mProjectionTypePropertyPaths since
                 // the builder only replaces it, never adds to it. So even if the builder is used
                 // again, the previous one will remain with the object.
-                mProjectionTypePropertyPaths = new ArrayMap<>(mProjectionTypePropertyPaths);
+                mProjectionTypePropertyPaths = BundleUtil.deepCopy(mProjectionTypePropertyPaths);
                 mBuilt = false;
             }
         }
diff --git a/framework/java/external/android/app/appsearch/GetSchemaResponse.java b/framework/java/external/android/app/appsearch/GetSchemaResponse.java
index eb2986e..d7c63a2 100644
--- a/framework/java/external/android/app/appsearch/GetSchemaResponse.java
+++ b/framework/java/external/android/app/appsearch/GetSchemaResponse.java
@@ -22,7 +22,6 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
@@ -30,6 +29,8 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -39,6 +40,7 @@
 
 /** The response class of {@link AppSearchSession#getSchema} */
 @SafeParcelable.Class(creator = "GetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class GetSchemaResponse extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -86,8 +88,9 @@
      * access the schema. All keys in the map are prefixed with the package-database prefix. We do
      * lazy fetch, the object will be created when you first time fetch it. The Map is constructed
      * in ANY-ALL cases. The querier could read the {@link GenericDocument} objects under the {@code
-     * schemaType} if they holds ALL required permissions of ANY combinations. The value set
-     * represents {@link android.app.appsearch.SetSchemaRequest.AppSearchSupportedPermission}.
+     * schemaType} if they hold ALL required permissions of ANY combinations.
+     *
+     * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
      */
     @Nullable private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissionsCached;
 
@@ -299,6 +302,7 @@
     public static final class Builder {
         private int mVersion = 0;
         private ArrayList<AppSearchSchema> mSchemas = new ArrayList<>();
+
         /**
          * Creates the object when we actually set them. If we never set visibility settings, we
          * should throw {@link UnsupportedOperationException} in the visibility getters.
@@ -423,6 +427,8 @@
         // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
         @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
+        // @SetSchemaRequest is an IntDef annotation applied to Set<Set<Integer>>.
+        @SuppressWarnings("SupportAnnotationUsage")
         @NonNull
         public Builder setRequiredPermissionsForSchemaTypeVisibility(
                 @NonNull String schemaType,
@@ -448,6 +454,7 @@
          * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema
          */
         // Merged list available from getPubliclyVisibleSchemas
+        @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
         @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
         @NonNull
@@ -490,6 +497,7 @@
          *     call must to match to access the schema.
          */
         // Merged map available from getSchemasVisibleToConfigs
+        @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
         @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
         @NonNull
@@ -548,6 +556,10 @@
         @NonNull
         private InternalVisibilityConfig.Builder getOrCreateVisibilityConfigBuilder(
                 @NonNull String schemaType) {
+            if (mVisibilityConfigBuilders == null) {
+                throw new IllegalStateException(
+                        "GetSchemaResponse is not configured with" + "visibility setting support");
+            }
             InternalVisibilityConfig.Builder builder = mVisibilityConfigBuilders.get(schemaType);
             if (builder == null) {
                 builder = new InternalVisibilityConfig.Builder(schemaType);
diff --git a/framework/java/external/android/app/appsearch/InternalSetSchemaResponse.java b/framework/java/external/android/app/appsearch/InternalSetSchemaResponse.java
index ce63bc4..3d1d268 100644
--- a/framework/java/external/android/app/appsearch/InternalSetSchemaResponse.java
+++ b/framework/java/external/android/app/appsearch/InternalSetSchemaResponse.java
@@ -19,12 +19,13 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.Objects;
 
 /**
@@ -52,6 +53,7 @@
     private final SetSchemaResponse mSetSchemaResponse;
 
     @Field(id = 3, getter = "getErrorMessage")
+    @Nullable
     private final String mErrorMessage;
 
     @Constructor
@@ -74,7 +76,7 @@
     public static InternalSetSchemaResponse newSuccessfulSetSchemaResponse(
             @NonNull SetSchemaResponse setSchemaResponse) {
         return new InternalSetSchemaResponse(
-                /*isSuccess=*/ true, setSchemaResponse, /*errorMessage=*/ null);
+                /* isSuccess= */ true, setSchemaResponse, /* errorMessage= */ null);
     }
 
     /**
@@ -86,7 +88,8 @@
     @NonNull
     public static InternalSetSchemaResponse newFailedSetSchemaResponse(
             @NonNull SetSchemaResponse setSchemaResponse, @NonNull String errorMessage) {
-        return new InternalSetSchemaResponse(/*isSuccess=*/ false, setSchemaResponse, errorMessage);
+        return new InternalSetSchemaResponse(
+                /* isSuccess= */ false, setSchemaResponse, errorMessage);
     }
 
     /** Returns {@code true} if the schema request is proceeded successfully. */
diff --git a/framework/java/external/android/app/appsearch/InternalVisibilityConfig.java b/framework/java/external/android/app/appsearch/InternalVisibilityConfig.java
index 99379e3..c4544dc 100644
--- a/framework/java/external/android/app/appsearch/InternalVisibilityConfig.java
+++ b/framework/java/external/android/app/appsearch/InternalVisibilityConfig.java
@@ -20,13 +20,14 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -167,9 +168,16 @@
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof InternalVisibilityConfig)) return false;
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null) {
+            return false;
+        }
+        if (!(o instanceof InternalVisibilityConfig)) {
+            return false;
+        }
         InternalVisibilityConfig that = (InternalVisibilityConfig) o;
         return mIsNotDisplayedBySystem == that.mIsNotDisplayedBySystem
                 && Objects.equals(mSchemaType, that.mSchemaType)
@@ -307,6 +315,7 @@
          *
          * @see SchemaVisibilityConfig.Builder#setPubliclyVisibleTargetPackage
          */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setPubliclyVisibleTargetPackage(
                 @Nullable PackageIdentifier packageIdentifier) {
@@ -325,8 +334,8 @@
          * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} hold all requirements
          *     that a call must match to access the schema.
          */
-        @NonNull
         @CanIgnoreReturnValue
+        @NonNull
         public Builder addVisibleToConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) {
             Objects.requireNonNull(schemaVisibilityConfig);
             resetIfBuilt();
@@ -335,8 +344,8 @@
         }
 
         /** Clears the set of {@link SchemaVisibilityConfig} which have access to this schema. */
-        @NonNull
         @CanIgnoreReturnValue
+        @NonNull
         public Builder clearVisibleToConfig() {
             resetIfBuilt();
             mVisibleToConfigs.clear();
diff --git a/framework/java/external/android/app/appsearch/JoinSpec.java b/framework/java/external/android/app/appsearch/JoinSpec.java
index d16fc12..4337588 100644
--- a/framework/java/external/android/app/appsearch/JoinSpec.java
+++ b/framework/java/external/android/app/appsearch/JoinSpec.java
@@ -19,14 +19,13 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -113,6 +112,7 @@
  * nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}.
  */
 @SafeParcelable.Class(creator = "JoinSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class JoinSpec extends AbstractSafeParcelable {
     /** Creator class for {@link JoinSpec}. */
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -171,14 +171,19 @@
      * perform a join, but keep the parent ranking signal.
      */
     public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0;
+
     /** Score the aggregation of joined documents by counting the number of results. */
     public static final int AGGREGATION_SCORING_RESULT_COUNT = 1;
+
     /** Score the aggregation of joined documents using the smallest ranking signal. */
     public static final int AGGREGATION_SCORING_MIN_RANKING_SIGNAL = 2;
+
     /** Score the aggregation of joined documents using the average ranking signal. */
     public static final int AGGREGATION_SCORING_AVG_RANKING_SIGNAL = 3;
+
     /** Score the aggregation of joined documents using the largest ranking signal. */
     public static final int AGGREGATION_SCORING_MAX_RANKING_SIGNAL = 4;
+
     /** Score the aggregation of joined documents using the sum of ranking signal. */
     public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5;
 
@@ -186,12 +191,12 @@
     JoinSpec(
             @Param(id = 1) @NonNull String nestedQuery,
             @Param(id = 2) @NonNull SearchSpec nestedSearchSpec,
-            @Param(id = 3) @Nullable String childPropertyExpression,
+            @Param(id = 3) @NonNull String childPropertyExpression,
             @Param(id = 4) int maxJoinedResultCount,
             @Param(id = 5) @AggregationScoringStrategy int aggregationScoringStrategy) {
         mNestedQuery = Objects.requireNonNull(nestedQuery);
         mNestedSearchSpec = Objects.requireNonNull(nestedSearchSpec);
-        mChildPropertyExpression = childPropertyExpression;
+        mChildPropertyExpression = Objects.requireNonNull(childPropertyExpression);
         mMaxJoinedResultCount = maxJoinedResultCount;
         mAggregationScoringStrategy = aggregationScoringStrategy;
     }
diff --git a/framework/java/external/android/app/appsearch/PropertyPath.java b/framework/java/external/android/app/appsearch/PropertyPath.java
index be8c232..6c19fff 100644
--- a/framework/java/external/android/app/appsearch/PropertyPath.java
+++ b/framework/java/external/android/app/appsearch/PropertyPath.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.app.appsearch.checker.initialization.qual.UnderInitialization;
+import android.app.appsearch.checker.nullness.qual.RequiresNonNull;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -72,7 +74,9 @@
         }
     }
 
-    private void recursivePathScan(String path) throws IllegalArgumentException {
+    @RequiresNonNull("mPathList")
+    private void recursivePathScan(@UnderInitialization PropertyPath this, String path)
+            throws IllegalArgumentException {
         // Determine whether the path is just a raw property name with no control characters
         int controlPos = -1;
         boolean controlIsIndex = false;
@@ -128,7 +132,9 @@
      * @return the rest of the path after the end brackets, or null if there is nothing after them
      */
     @Nullable
-    private String consumePropertyWithIndex(@NonNull String path, int controlPos) {
+    @RequiresNonNull("mPathList")
+    private String consumePropertyWithIndex(
+            @UnderInitialization PropertyPath this, @NonNull String path, int controlPos) {
         Objects.requireNonNull(path);
         String propertyName = path.substring(0, controlPos);
         int endBracketIdx = path.indexOf(']', controlPos);
@@ -210,17 +216,23 @@
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null) return false;
-        if (!(o instanceof PropertyPath)) return false;
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null) {
+            return false;
+        }
+        if (!(o instanceof PropertyPath)) {
+            return false;
+        }
         PropertyPath that = (PropertyPath) o;
         return Objects.equals(mPathList, that.mPathList);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mPathList);
+        return Objects.hashCode(mPathList);
     }
 
     /**
@@ -297,9 +309,7 @@
             mPropertyIndex = propertyIndex;
         }
 
-        /**
-         * @return the property name
-         */
+        /** Returns the name of the property. */
         @NonNull
         public String getPropertyName() {
             return mPropertyName;
@@ -324,10 +334,16 @@
         }
 
         @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null) return false;
-            if (!(o instanceof PathSegment)) return false;
+        public boolean equals(@Nullable Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null) {
+                return false;
+            }
+            if (!(o instanceof PathSegment)) {
+                return false;
+            }
             PathSegment that = (PathSegment) o;
             return mPropertyIndex == that.mPropertyIndex
                     && mPropertyName.equals(that.mPropertyName);
diff --git a/framework/java/external/android/app/appsearch/PutDocumentsRequest.java b/framework/java/external/android/app/appsearch/PutDocumentsRequest.java
index d51b84a..c355dbe 100644
--- a/framework/java/external/android/app/appsearch/PutDocumentsRequest.java
+++ b/framework/java/external/android/app/appsearch/PutDocumentsRequest.java
@@ -21,9 +21,10 @@
 import android.annotation.NonNull;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
 import android.app.appsearch.exceptions.AppSearchException;
-import android.app.appsearch.flags.Flags;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -95,39 +96,62 @@
          * Adds one or more {@link GenericDocument} objects containing taken action metrics to the
          * request.
          *
-         * <p>Metrics to be collected by AppSearch:
+         * <p>It is recommended to use taken action document classes in Jetpack library to construct
+         * taken action documents.
+         *
+         * <p>The document creation timestamp of the {@link GenericDocument} should be set to the
+         * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}.
+         *
+         * <p>Clients should report search and click actions together sorted by {@link
+         * GenericDocument#getCreationTimestampMillis} in chronological order.
+         *
+         * <p>For example, if there are 2 search actions, with 1 click action associated with the
+         * first and 2 click actions associated with the second, then clients should report
+         * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].
+         *
+         * <p>Different types of taken actions and metrics to be collected by AppSearch:
          *
          * <ul>
-         *   <li>name: STRING, the name of the taken action.
-         *       <p>Name is an optional custom field that allows the client to tag and categorize
-         *       taken action {@link GenericDocument}.
-         *   <li>referencedQualifiedId: STRING, the qualified id of the {@link SearchResult}
-         *       document that the user takes action on.
-         *       <p>A qualified id is a string generated by package, database, namespace, and
-         *       document id. See {@link
-         *       android.app.appsearch.util.DocumentIdUtil#createQualifiedId} for more details.
-         *   <li>previousQueries: REPEATED STRING, the list of all previous user-entered search
-         *       inputs, without any operators or rewriting, collected during this search session in
-         *       chronological order.
-         *   <li>finalQuery: STRING, the final user-entered search input (without any operators or
-         *       rewriting) that yielded the {@link SearchResult} on which the user took action.
-         *   <li>resultRankInBlock: LONG, the rank of the {@link SearchResult} document among the
-         *       user-defined block.
-         *       <p>The client can define its own custom definition for block, e.g. corpus name,
-         *       group, etc.
-         *       <p>For example, a client defines the block as corpus, and AppSearch returns 5
-         *       documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"].
-         *       Then the block ranks of them = [1, 2, 1, 1, 2].
-         *       <p>If the client is not presenting the results in multiple blocks, they should set
-         *       this value to match resultRankGlobal.
-         *   <li>resultRankGlobal: LONG, the global rank of the {@link SearchResult} document.
-         *       <p>Global rank reflects the order of {@link SearchResult} documents returned by
-         *       AppSearch.
-         *       <p>For example, AppSearch returns 2 pages with 10 {@link SearchResult} documents
-         *       for each page. Then the global ranks of them will be 1 to 10 for the first page,
-         *       and 11 to 20 for the second page.
-         *   <li>timeStayOnResultMillis: LONG, the time in milliseconds that user stays on the
-         *       {@link SearchResult} document after clicking it.
+         *   <li>Search action
+         *       <ul>
+         *         <li>actionType: LONG, the enum value of the action type.
+         *             <p>Requires to be {@code 1} for search actions.
+         *         <li>query: STRING, the user-entered search input (without any operators or
+         *             rewriting).
+         *         <li>fetchedResultCount: LONG, the number of {@link SearchResult} documents
+         *             fetched from AppSearch in this search action.
+         *       </ul>
+         *   <li>Click action
+         *       <ul>
+         *         <li>actionType: LONG, the enum value of the action type.
+         *             <p>Requires to be {@code 2} for click actions.
+         *         <li>query: STRING, the user-entered search input (without any operators or
+         *             rewriting) that yielded the {@link SearchResult} on which the user took
+         *             action.
+         *         <li>referencedQualifiedId: STRING, the qualified id of the {@link SearchResult}
+         *             document that the user takes action on.
+         *             <p>A qualified id is a string generated by package, database, namespace, and
+         *             document id. See {@link
+         *             android.app.appsearch.util.DocumentIdUtil#createQualifiedId} for more
+         *             details.
+         *         <li>resultRankInBlock: LONG, the rank of the {@link SearchResult} document among
+         *             the user-defined block.
+         *             <p>The client can define its own custom definition for block, for example,
+         *             corpus name, group, etc.
+         *             <p>For example, a client defines the block as corpus, and AppSearch returns 5
+         *             documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3",
+         *             "corpus2"]. Then the block ranks of them = [1, 2, 1, 1, 2].
+         *             <p>If the client is not presenting the results in multiple blocks, they
+         *             should set this value to match resultRankGlobal.
+         *         <li>resultRankGlobal: LONG, the global rank of the {@link SearchResult} document.
+         *             <p>Global rank reflects the order of {@link SearchResult} documents returned
+         *             by AppSearch.
+         *             <p>For example, AppSearch returns 2 pages with 10 {@link SearchResult}
+         *             documents for each page. Then the global ranks of them will be 1 to 10 for
+         *             the first page, and 11 to 20 for the second page.
+         *         <li>timeStayOnResultMillis: LONG, the time in milliseconds that user stays on the
+         *             {@link SearchResult} document after clicking it.
+         *       </ul>
          * </ul>
          *
          * <p>Certain anonymized information about actions reported using this API may be uploaded
diff --git a/framework/java/external/android/app/appsearch/RemoveByDocumentIdRequest.java b/framework/java/external/android/app/appsearch/RemoveByDocumentIdRequest.java
index 81a0b3a..cc09ade 100644
--- a/framework/java/external/android/app/appsearch/RemoveByDocumentIdRequest.java
+++ b/framework/java/external/android/app/appsearch/RemoveByDocumentIdRequest.java
@@ -16,13 +16,23 @@
 
 package android.app.appsearch;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 
@@ -32,13 +42,36 @@
  *
  * @see AppSearchSession#remove
  */
-public final class RemoveByDocumentIdRequest {
-    private final String mNamespace;
-    private final Set<String> mIds;
[email protected](creator = "RemoveByDocumentIdRequestCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class RemoveByDocumentIdRequest extends AbstractSafeParcelable {
+    /** Creator class for {@link android.app.appsearch.RemoveByDocumentIdRequest}. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<RemoveByDocumentIdRequest> CREATOR =
+            new RemoveByDocumentIdRequestCreator();
 
-    RemoveByDocumentIdRequest(String namespace, Set<String> ids) {
-        mNamespace = namespace;
-        mIds = ids;
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
+    private final String mNamespace;
+
+    @NonNull
+    @Field(id = 2)
+    final List<String> mIds;
+
+    @Nullable private Set<String> mIdsCached;
+
+    /**
+     * Removes documents by ID.
+     *
+     * @param namespace Namespace of the document to remove.
+     * @param ids The IDs of the documents to delete
+     */
+    @Constructor
+    RemoveByDocumentIdRequest(
+            @Param(id = 1) @NonNull String namespace, @Param(id = 2) @NonNull List<String> ids) {
+        mNamespace = Objects.requireNonNull(namespace);
+        mIds = Objects.requireNonNull(ids);
     }
 
     /** Returns the namespace to remove documents from. */
@@ -50,7 +83,16 @@
     /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set<String> getIds() {
-        return Collections.unmodifiableSet(mIds);
+        if (mIdsCached == null) {
+            mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+        }
+        return mIdsCached;
+    }
+
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        RemoveByDocumentIdRequestCreator.writeToParcel(this, dest, flags);
     }
 
     /** Builder for {@link RemoveByDocumentIdRequest} objects. */
@@ -87,7 +129,7 @@
         @NonNull
         public RemoveByDocumentIdRequest build() {
             mBuilt = true;
-            return new RemoveByDocumentIdRequest(mNamespace, mIds);
+            return new RemoveByDocumentIdRequest(mNamespace, new ArrayList<>(mIds));
         }
 
         private void resetIfBuilt() {
diff --git a/framework/java/external/android/app/appsearch/ReportUsageRequest.java b/framework/java/external/android/app/appsearch/ReportUsageRequest.java
index c5c1e70..753b6cd 100644
--- a/framework/java/external/android/app/appsearch/ReportUsageRequest.java
+++ b/framework/java/external/android/app/appsearch/ReportUsageRequest.java
@@ -17,8 +17,15 @@
 package android.app.appsearch;
 
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.safeparcel.AbstractSafeParcelable;
+import android.app.appsearch.safeparcel.SafeParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.appsearch.flags.Flags;
 
 import java.util.Objects;
 
@@ -29,13 +36,31 @@
  *
  * @see AppSearchSession#reportUsage
  */
-public final class ReportUsageRequest {
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "ReportUsageRequestCreator")
+public final class ReportUsageRequest extends AbstractSafeParcelable {
+
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator<ReportUsageRequest> CREATOR =
+            new ReportUsageRequestCreator();
+
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
     private final String mNamespace;
+
+    @NonNull
+    @Field(id = 2, getter = "getDocumentId")
     private final String mDocumentId;
+
+    @Field(id = 3, getter = "getUsageTimestampMillis")
     private final long mUsageTimestampMillis;
 
+    @Constructor
     ReportUsageRequest(
-            @NonNull String namespace, @NonNull String documentId, long usageTimestampMillis) {
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull String documentId,
+            @Param(id = 3) long usageTimestampMillis) {
         mNamespace = Objects.requireNonNull(namespace);
         mDocumentId = Objects.requireNonNull(documentId);
         mUsageTimestampMillis = usageTimestampMillis;
@@ -64,6 +89,12 @@
         return mUsageTimestampMillis;
     }
 
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        ReportUsageRequestCreator.writeToParcel(this, dest, flags);
+    }
+
     /** Builder for {@link ReportUsageRequest} objects. */
     public static final class Builder {
         private final String mNamespace;
diff --git a/framework/java/external/android/app/appsearch/SchemaVisibilityConfig.java b/framework/java/external/android/app/appsearch/SchemaVisibilityConfig.java
index cee39b4..1cadc1e 100644
--- a/framework/java/external/android/app/appsearch/SchemaVisibilityConfig.java
+++ b/framework/java/external/android/app/appsearch/SchemaVisibilityConfig.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.PackageIdentifierParcel;
 import android.app.appsearch.safeparcel.SafeParcelable;
@@ -28,6 +27,8 @@
 import android.os.Parcelable;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -42,6 +43,7 @@
  */
 @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
 @SafeParcelable.Class(creator = "VisibilityConfigCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class SchemaVisibilityConfig extends AbstractSafeParcelable {
     @NonNull
     public static final Parcelable.Creator<SchemaVisibilityConfig> CREATOR =
@@ -86,9 +88,10 @@
     }
 
     /**
-     * Returns an array of Integers representing Android Permissions as defined in {@link
-     * SetSchemaRequest.AppSearchSupportedPermission} that the caller must hold to access the schema
-     * this {@link SchemaVisibilityConfig} represents.
+     * Returns an array of Integers representing Android Permissions that the caller must hold to
+     * access the schema this {@link SchemaVisibilityConfig} represents.
+     *
+     * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
      */
     @NonNull
     public Set<Set<Integer>> getRequiredPermissions() {
@@ -97,12 +100,13 @@
             for (int i = 0; i < mRequiredPermissions.size(); i++) {
                 VisibilityPermissionConfig permissionConfig = mRequiredPermissions.get(i);
                 Set<Integer> requiredPermissions = permissionConfig.getAllRequiredPermissions();
-                if (requiredPermissions != null) {
+                if (mRequiredPermissionsCached != null && requiredPermissions != null) {
                     mRequiredPermissionsCached.add(requiredPermissions);
                 }
             }
         }
-        return mRequiredPermissionsCached;
+        // Added for nullness checker as it is @Nullable, we initialize it above if it is null.
+        return Objects.requireNonNull(mRequiredPermissionsCached);
     }
 
     /**
@@ -125,9 +129,16 @@
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof SchemaVisibilityConfig)) return false;
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null) {
+            return false;
+        }
+        if (!(o instanceof SchemaVisibilityConfig)) {
+            return false;
+        }
         SchemaVisibilityConfig that = (SchemaVisibilityConfig) o;
         return Objects.equals(mAllowedPackages, that.mAllowedPackages)
                 && Objects.equals(mRequiredPermissions, that.mRequiredPermissions)
@@ -150,7 +161,7 @@
     public static final class Builder {
         private List<PackageIdentifierParcel> mAllowedPackages = new ArrayList<>();
         private List<VisibilityPermissionConfig> mRequiredPermissions = new ArrayList<>();
-        private PackageIdentifierParcel mPubliclyVisibleTargetPackage;
+        @Nullable private PackageIdentifierParcel mPubliclyVisibleTargetPackage;
         private boolean mBuilt;
 
         /** Creates a {@link Builder} for a {@link SchemaVisibilityConfig}. */
@@ -243,6 +254,7 @@
          *     android.content.pm.PackageManager#canPackageQuery} to determine which packages can
          *     access this publicly visible schema.
          */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setPubliclyVisibleTargetPackage(
                 @Nullable PackageIdentifier packageIdentifier) {
diff --git a/framework/java/external/android/app/appsearch/SearchResult.java b/framework/java/external/android/app/appsearch/SearchResult.java
index fddeb88..0e297c5 100644
--- a/framework/java/external/android/app/appsearch/SearchResult.java
+++ b/framework/java/external/android/app/appsearch/SearchResult.java
@@ -20,16 +20,17 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
@@ -50,6 +51,7 @@
  * @see SearchResults
  */
 @SafeParcelable.Class(creator = "SearchResultCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class SearchResult extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -74,6 +76,10 @@
     @Field(id = 6, getter = "getJoinedResults")
     private final List<SearchResult> mJoinedResults;
 
+    @NonNull
+    @Field(id = 7, getter = "getInformationalRankingSignals")
+    private final List<Double> mInformationalRankingSignals;
+
     /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */
     @Nullable private GenericDocument mDocumentCached;
 
@@ -88,13 +94,20 @@
             @Param(id = 3) @NonNull String packageName,
             @Param(id = 4) @NonNull String databaseName,
             @Param(id = 5) double rankingSignal,
-            @Param(id = 6) @NonNull List<SearchResult> joinedResults) {
+            @Param(id = 6) @NonNull List<SearchResult> joinedResults,
+            @Param(id = 7) @Nullable List<Double> informationalRankingSignals) {
         mDocument = Objects.requireNonNull(document);
         mMatchInfos = Objects.requireNonNull(matchInfos);
         mPackageName = Objects.requireNonNull(packageName);
         mDatabaseName = Objects.requireNonNull(databaseName);
         mRankingSignal = rankingSignal;
-        mJoinedResults = Objects.requireNonNull(joinedResults);
+        mJoinedResults = Collections.unmodifiableList(Objects.requireNonNull(joinedResults));
+        if (informationalRankingSignals != null) {
+            mInformationalRankingSignals =
+                    Collections.unmodifiableList(informationalRankingSignals);
+        } else {
+            mInformationalRankingSignals = Collections.emptyList();
+        }
     }
 
     /**
@@ -131,6 +144,7 @@
                     mMatchInfosCached.add(matchInfo);
                 }
             }
+            mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached);
         }
         // This check is added for NullnessChecker, mMatchInfos will always be NonNull.
         return Objects.requireNonNull(mMatchInfosCached);
@@ -187,6 +201,16 @@
     }
 
     /**
+     * Returns the informational ranking signals of the {@link GenericDocument}, according to the
+     * expressions added in {@link SearchSpec.Builder#addInformationalRankingExpressions}.
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public List<Double> getInformationalRankingSignals() {
+        return mInformationalRankingSignals;
+    }
+
+    /**
      * Gets a list of {@link SearchResult} joined from the join operation.
      *
      * <p>These joined documents match the outer document as specified in the {@link JoinSpec} with
@@ -215,10 +239,11 @@
     public static final class Builder {
         private final String mPackageName;
         private final String mDatabaseName;
-        private ArrayList<MatchInfo> mMatchInfos = new ArrayList<>();
+        private List<MatchInfo> mMatchInfos = new ArrayList<>();
         private GenericDocument mGenericDocument;
         private double mRankingSignal;
-        private ArrayList<SearchResult> mJoinedResults = new ArrayList<>();
+        private List<Double> mInformationalRankingSignals = new ArrayList<>();
+        private List<SearchResult> mJoinedResults = new ArrayList<>();
         private boolean mBuilt = false;
 
         /**
@@ -237,12 +262,14 @@
             Objects.requireNonNull(searchResult);
             mPackageName = searchResult.getPackageName();
             mDatabaseName = searchResult.getDatabaseName();
+            mGenericDocument = searchResult.getGenericDocument();
+            mRankingSignal = searchResult.getRankingSignal();
+            mInformationalRankingSignals =
+                    new ArrayList<>(searchResult.getInformationalRankingSignals());
             List<MatchInfo> matchInfos = searchResult.getMatchInfos();
             for (int i = 0; i < matchInfos.size(); i++) {
                 addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build());
             }
-            mGenericDocument = searchResult.getGenericDocument();
-            mRankingSignal = searchResult.getRankingSignal();
             List<SearchResult> joinedResults = searchResult.getJoinedResults();
             for (int i = 0; i < joinedResults.size(); i++) {
                 addJoinedResult(joinedResults.get(i));
@@ -281,6 +308,16 @@
             return this;
         }
 
+        /** Adds the informational ranking signal of the matched document in this SearchResult. */
+        @CanIgnoreReturnValue
+        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+        @NonNull
+        public Builder addInformationalRankingSignal(double rankingSignal) {
+            resetIfBuilt();
+            mInformationalRankingSignals.add(rankingSignal);
+            return this;
+        }
+
         /**
          * Adds a {@link SearchResult} that was joined by the {@link JoinSpec}.
          *
@@ -317,13 +354,15 @@
                     mPackageName,
                     mDatabaseName,
                     mRankingSignal,
-                    mJoinedResults);
+                    mJoinedResults,
+                    mInformationalRankingSignals);
         }
 
         private void resetIfBuilt() {
             if (mBuilt) {
                 mMatchInfos = new ArrayList<>(mMatchInfos);
                 mJoinedResults = new ArrayList<>(mJoinedResults);
+                mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals);
                 mBuilt = false;
             }
         }
@@ -405,6 +444,7 @@
      * </ul>
      */
     @SafeParcelable.Class(creator = "MatchInfoCreator")
+    @SuppressWarnings("HiddenSuperclass")
     public static final class MatchInfo extends AbstractSafeParcelable {
 
         @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
diff --git a/framework/java/external/android/app/appsearch/SearchSpec.java b/framework/java/external/android/app/appsearch/SearchSpec.java
index 510aeed..0622bcb 100644
--- a/framework/java/external/android/app/appsearch/SearchSpec.java
+++ b/framework/java/external/android/app/appsearch/SearchSpec.java
@@ -24,7 +24,6 @@
 import android.annotation.SuppressLint;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
 import android.app.appsearch.exceptions.AppSearchException;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.app.appsearch.util.BundleUtil;
@@ -34,6 +33,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -52,6 +52,7 @@
  * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
  */
 @SafeParcelable.Class(creator = "SearchSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class SearchSpec extends AbstractSafeParcelable {
 
     /** Creator class for {@link SearchSpec}. */
@@ -134,9 +135,25 @@
     private final List<String> mEnabledFeatures;
 
     @Field(id = 19, getter = "getSearchSourceLogTag")
+    @Nullable
     private final String mSearchSourceLogTag;
 
-    /** @hide */
+    @NonNull
+    @Field(id = 20, getter = "getSearchEmbeddings")
+    private final List<EmbeddingVector> mSearchEmbeddings;
+
+    @Field(id = 21, getter = "getDefaultEmbeddingSearchMetricType")
+    private final int mDefaultEmbeddingSearchMetricType;
+
+    @NonNull
+    @Field(id = 22, getter = "getInformationalRankingExpressions")
+    private final List<String> mInformationalRankingExpressions;
+
+    /**
+     * Default number of documents per page.
+     *
+     * @hide
+     */
     public static final int DEFAULT_NUM_PER_PAGE = 10;
 
     // TODO(b/170371356): In framework, we may want these limits to be flag controlled.
@@ -165,6 +182,7 @@
      * "football".
      */
     public static final int TERM_MATCH_EXACT_ONLY = 1;
+
     /**
      * Query terms will match indexed tokens when the query term is a prefix of the token.
      *
@@ -199,20 +217,28 @@
 
     /** No Ranking, results are returned in arbitrary order. */
     public static final int RANKING_STRATEGY_NONE = 0;
+
     /** Ranked by app-provided document scores. */
     public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
+
     /** Ranked by document creation timestamps. */
     public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
+
     /** Ranked by document relevance score. */
     public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
+
     /** Ranked by number of usages, as reported by the app. */
     public static final int RANKING_STRATEGY_USAGE_COUNT = 4;
+
     /** Ranked by timestamp of last usage, as reported by the app. */
     public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5;
+
     /** Ranked by number of usages from a system UI surface. */
     public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6;
+
     /** Ranked by timestamp of last usage from a system UI surface. */
     public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7;
+
     /**
      * Ranked by the aggregated ranking signal of the joined documents.
      *
@@ -223,6 +249,7 @@
      * @see Builder#build
      */
     public static final int RANKING_STRATEGY_JOIN_AGGREGATE_SCORE = 8;
+
     /** Ranked by the advanced ranking expression provided. */
     public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9;
 
@@ -240,6 +267,7 @@
 
     /** Search results will be returned in a descending order. */
     public static final int ORDER_DESCENDING = 0;
+
     /** Search results will be returned in an ascending order. */
     public static final int ORDER_ASCENDING = 1;
 
@@ -257,16 +285,19 @@
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface GroupingType {}
+
     /**
      * Results should be grouped together by package for the purpose of enforcing a limit on the
      * number of results returned per package.
      */
     public static final int GROUPING_TYPE_PER_PACKAGE = 1 << 0;
+
     /**
      * Results should be grouped together by namespace for the purpose of enforcing a limit on the
      * number of results returned per namespace.
      */
     public static final int GROUPING_TYPE_PER_NAMESPACE = 1 << 1;
+
     /**
      * Results should be grouped together by schema type for the purpose of enforcing a limit on the
      * number of results returned per schema type.
@@ -274,6 +305,36 @@
     @FlaggedApi(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA)
     public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;
 
+    /**
+     * Type of scoring used to calculate similarity for embedding vectors. For details of each, see
+     * comments above each value.
+     *
+     * @hide
+     */
+    // NOTE: The integer values of these constants must match the proto enum constants in
+    // {@link SearchSpecProto.EmbeddingQueryMetricType.Code}
+
+    @IntDef(
+            value = {
+                EMBEDDING_SEARCH_METRIC_TYPE_COSINE,
+                EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT,
+                EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EmbeddingSearchMetricType {}
+
+    /** Cosine similarity as metric for embedding search and ranking. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1;
+
+    /** Dot product similarity as metric for embedding search and ranking. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2;
+
+    /** Euclidean distance as metric for embedding search and ranking. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public static final int EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN = 3;
+
     @Constructor
     SearchSpec(
             @Param(id = 1) int termMatchType,
@@ -294,12 +355,15 @@
             @Param(id = 16) @Nullable JoinSpec joinSpec,
             @Param(id = 17) @NonNull String advancedRankingExpression,
             @Param(id = 18) @NonNull List<String> enabledFeatures,
-            @Param(id = 19) @Nullable String searchSourceLogTag) {
+            @Param(id = 19) @Nullable String searchSourceLogTag,
+            @Param(id = 20) @Nullable List<EmbeddingVector> searchEmbeddings,
+            @Param(id = 21) int defaultEmbeddingSearchMetricType,
+            @Param(id = 22) @Nullable List<String> informationalRankingExpressions) {
         mTermMatchType = termMatchType;
-        mSchemas = Objects.requireNonNull(schemas);
-        mNamespaces = Objects.requireNonNull(namespaces);
+        mSchemas = Collections.unmodifiableList(Objects.requireNonNull(schemas));
+        mNamespaces = Collections.unmodifiableList(Objects.requireNonNull(namespaces));
         mTypePropertyFilters = Objects.requireNonNull(properties);
-        mPackageNames = Objects.requireNonNull(packageNames);
+        mPackageNames = Collections.unmodifiableList(Objects.requireNonNull(packageNames));
         mResultCountPerPage = resultCountPerPage;
         mRankingStrategy = rankingStrategy;
         mOrder = order;
@@ -312,8 +376,20 @@
         mTypePropertyWeightsField = Objects.requireNonNull(typePropertyWeightsField);
         mJoinSpec = joinSpec;
         mAdvancedRankingExpression = Objects.requireNonNull(advancedRankingExpression);
-        mEnabledFeatures = Objects.requireNonNull(enabledFeatures);
+        mEnabledFeatures = Collections.unmodifiableList(Objects.requireNonNull(enabledFeatures));
         mSearchSourceLogTag = searchSourceLogTag;
+        if (searchEmbeddings != null) {
+            mSearchEmbeddings = Collections.unmodifiableList(searchEmbeddings);
+        } else {
+            mSearchEmbeddings = Collections.emptyList();
+        }
+        mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;
+        if (informationalRankingExpressions != null) {
+            mInformationalRankingExpressions =
+                    Collections.unmodifiableList(informationalRankingExpressions);
+        } else {
+            mInformationalRankingExpressions = Collections.emptyList();
+        }
     }
 
     /** Returns how the query terms should match terms in the index. */
@@ -332,7 +408,7 @@
         if (mSchemas == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(mSchemas);
+        return mSchemas;
     }
 
     /**
@@ -366,7 +442,7 @@
         if (mNamespaces == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(mNamespaces);
+        return mNamespaces;
     }
 
     /**
@@ -381,7 +457,7 @@
         if (mPackageNames == null) {
             return Collections.emptyList();
         }
-        return Collections.unmodifiableList(mPackageNames);
+        return mPackageNames;
     }
 
     /** Returns the number of results per page in the result set. */
@@ -458,11 +534,14 @@
         for (String schema : schemas) {
             ArrayList<String> propertyPathList =
                     mProjectionTypePropertyMasks.getStringArrayList(schema);
-            List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
-            for (String p : propertyPathList) {
-                copy.add(new PropertyPath(p));
+            if (propertyPathList != null) {
+                List<PropertyPath> copy = new ArrayList<>(propertyPathList.size());
+                for (int i = 0; i < propertyPathList.size(); i++) {
+                    String p = propertyPathList.get(i);
+                    copy.add(new PropertyPath(p));
+                }
+                typePropertyPathsMap.put(schema, copy);
             }
-            typePropertyPathsMap.put(schema, copy);
         }
         return typePropertyPathsMap;
     }
@@ -483,12 +562,15 @@
                 new ArrayMap<>(schemaTypes.size());
         for (String schemaType : schemaTypes) {
             Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);
-            Set<String> propertyPaths = propertyPathBundle.keySet();
-            Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
-            for (String propertyPath : propertyPaths) {
-                propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath));
+            if (propertyPathBundle != null) {
+                Set<String> propertyPaths = propertyPathBundle.keySet();
+                Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+                for (String propertyPath : propertyPaths) {
+                    propertyPathWeights.put(
+                            propertyPath, propertyPathBundle.getDouble(propertyPath));
+                }
+                typePropertyWeightsMap.put(schemaType, propertyPathWeights);
             }
-            typePropertyWeightsMap.put(schemaType, propertyPathWeights);
         }
         return typePropertyWeightsMap;
     }
@@ -509,13 +591,17 @@
                 new ArrayMap<>(schemaTypes.size());
         for (String schemaType : schemaTypes) {
             Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType);
-            Set<String> propertyPaths = propertyPathBundle.keySet();
-            Map<PropertyPath, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
-            for (String propertyPath : propertyPaths) {
-                propertyPathWeights.put(
-                        new PropertyPath(propertyPath), propertyPathBundle.getDouble(propertyPath));
+            if (propertyPathBundle != null) {
+                Set<String> propertyPaths = propertyPathBundle.keySet();
+                Map<PropertyPath, Double> propertyPathWeights =
+                        new ArrayMap<>(propertyPaths.size());
+                for (String propertyPath : propertyPaths) {
+                    propertyPathWeights.put(
+                            new PropertyPath(propertyPath),
+                            propertyPathBundle.getDouble(propertyPath));
+                }
+                typePropertyWeightsMap.put(schemaType, propertyPathWeights);
             }
-            typePropertyWeightsMap.put(schemaType, propertyPathWeights);
         }
         return typePropertyWeightsMap;
     }
@@ -574,6 +660,35 @@
         return mSearchSourceLogTag;
     }
 
+    /** Returns the list of {@link EmbeddingVector} for embedding search. */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public List<EmbeddingVector> getSearchEmbeddings() {
+        return mSearchEmbeddings;
+    }
+
+    /**
+     * Returns the default embedding metric type used for embedding search (see {@link
+     * AppSearchSession#search}) and ranking (see {@link
+     * SearchSpec.Builder#setRankingStrategy(String)}).
+     */
+    @EmbeddingSearchMetricType
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public int getDefaultEmbeddingSearchMetricType() {
+        return mDefaultEmbeddingSearchMetricType;
+    }
+
+    /**
+     * Returns the informational ranking expressions.
+     *
+     * @see Builder#addInformationalRankingExpressions
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public List<String> getInformationalRankingExpressions() {
+        return mInformationalRankingExpressions;
+    }
+
     /** Returns whether the NUMERIC_SEARCH feature is enabled. */
     public boolean isNumericSearchEnabled() {
         return mEnabledFeatures.contains(FeatureConstants.NUMERIC_SEARCH);
@@ -595,6 +710,18 @@
         return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION);
     }
 
+    /** Returns whether the embedding search feature is enabled. */
+    @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public boolean isEmbeddingSearchEnabled() {
+        return mEnabledFeatures.contains(FeatureConstants.EMBEDDING_SEARCH);
+    }
+
+    /** Returns whether the LIST_FILTER_TOKENIZE_FUNCTION feature is enabled. */
+    @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public boolean isListFilterTokenizeFunctionEnabled() {
+        return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
+    }
+
     /**
      * Get the list of enabled features that the caller is intending to use in this search call.
      *
@@ -614,16 +741,21 @@
 
     /** Builder for {@link SearchSpec objects}. */
     public static final class Builder {
-        private ArrayList<String> mSchemas = new ArrayList<>();
-        private ArrayList<String> mNamespaces = new ArrayList<>();
+        private List<String> mSchemas = new ArrayList<>();
+        private List<String> mNamespaces = new ArrayList<>();
         private Bundle mTypePropertyFilters = new Bundle();
-        private ArrayList<String> mPackageNames = new ArrayList<>();
+        private List<String> mPackageNames = new ArrayList<>();
         private ArraySet<String> mEnabledFeatures = new ArraySet<>();
         private Bundle mProjectionTypePropertyMasks = new Bundle();
         private Bundle mTypePropertyWeights = new Bundle();
+        private List<EmbeddingVector> mSearchEmbeddings = new ArrayList<>();
 
         private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
         @TermMatch private int mTermMatchType = TERM_MATCH_PREFIX;
+
+        @EmbeddingSearchMetricType
+        private int mDefaultEmbeddingSearchMetricType = EMBEDDING_SEARCH_METRIC_TYPE_COSINE;
+
         private int mSnippetCount = 0;
         private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT;
         private int mMaxSnippetSize = 0;
@@ -631,8 +763,9 @@
         @Order private int mOrder = ORDER_DESCENDING;
         @GroupingType private int mGroupingTypeFlags = 0;
         private int mGroupingLimit = 0;
-        private JoinSpec mJoinSpec;
+        @Nullable private JoinSpec mJoinSpec;
         private String mAdvancedRankingExpression = "";
+        private List<String> mInformationalRankingExpressions = new ArrayList<>();
         @Nullable private String mSearchSourceLogTag;
         private boolean mBuilt = false;
 
@@ -657,8 +790,10 @@
                     searchSpec.getPropertyWeights().entrySet()) {
                 setPropertyWeights(entry.getKey(), entry.getValue());
             }
+            mSearchEmbeddings = new ArrayList<>(searchSpec.getSearchEmbeddings());
             mResultCountPerPage = searchSpec.getResultCountPerPage();
             mTermMatchType = searchSpec.getTermMatch();
+            mDefaultEmbeddingSearchMetricType = searchSpec.getDefaultEmbeddingSearchMetricType();
             mSnippetCount = searchSpec.getSnippetCount();
             mSnippetCountPerProperty = searchSpec.getSnippetCountPerProperty();
             mMaxSnippetSize = searchSpec.getMaxSnippetSize();
@@ -668,6 +803,8 @@
             mGroupingLimit = searchSpec.getResultGroupingLimit();
             mJoinSpec = searchSpec.getJoinSpec();
             mAdvancedRankingExpression = searchSpec.getAdvancedRankingExpression();
+            mInformationalRankingExpressions =
+                    new ArrayList<>(searchSpec.getInformationalRankingExpressions());
             mSearchSourceLogTag = searchSpec.getSearchSourceLogTag();
         }
 
@@ -738,6 +875,7 @@
          * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited sequence
          *     of property names.
          */
+        @CanIgnoreReturnValue
         @NonNull
         @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public Builder addFilterProperties(
@@ -952,6 +1090,19 @@
          *       current document being scored. Property weights come from what's specified in
          *       {@link SearchSpec}. After normalizing, each provided weight will be divided by the
          *       maximum weight, so that each of them will be <= 1.
+         *   <li>this.matchedSemanticScores(getSearchSpecEmbedding({embedding_index}), {metric})
+         *       <p>Returns a list of the matched similarity scores from "semanticSearch" in the
+         *       query expression (see also {@link AppSearchSession#search}) based on
+         *       embedding_index and metric. If metric is omitted, it defaults to the metric
+         *       specified in {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}.
+         *       If no "semanticSearch" is called for embedding_index and metric in the query, this
+         *       function will return an empty list. If multiple "semanticSearch"s are called for
+         *       the same embedding_index and metric, this function will return a list of their
+         *       merged scores.
+         *       <p>Example: `this.matchedSemanticScores(getSearchSpecEmbedding(0), "COSINE")` will
+         *       return a list of matched scores within the range of [0.5, 1], if
+         *       `semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")` is called in the
+         *       query expression.
          * </ul>
          *
          * <p>Some errors may occur when using advanced ranking.
@@ -1009,6 +1160,46 @@
         }
 
         /**
+         * Adds informational ranking expressions to be evaluated for each document in the search
+         * result. The values of these expressions will be returned to the caller via {@link
+         * SearchResult#getInformationalRankingSignals()}. These expressions are purely for the
+         * caller to retrieve additional information about the result and have no effect on ranking.
+         *
+         * <p>The syntax is exactly the same as specified in {@link
+         * SearchSpec.Builder#setRankingStrategy(String)}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+        public Builder addInformationalRankingExpressions(
+                @NonNull String... informationalRankingExpressions) {
+            Objects.requireNonNull(informationalRankingExpressions);
+            resetIfBuilt();
+            return addInformationalRankingExpressions(
+                    Arrays.asList(informationalRankingExpressions));
+        }
+
+        /**
+         * Adds informational ranking expressions to be evaluated for each document in the search
+         * result. The values of these expressions will be returned to the caller via {@link
+         * SearchResult#getInformationalRankingSignals()}. These expressions are purely for the
+         * caller to retrieve additional information about the result and have no effect on ranking.
+         *
+         * <p>The syntax is exactly the same as specified in {@link
+         * SearchSpec.Builder#setRankingStrategy(String)}.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+        public Builder addInformationalRankingExpressions(
+                @NonNull Collection<String> informationalRankingExpressions) {
+            Objects.requireNonNull(informationalRankingExpressions);
+            resetIfBuilt();
+            mInformationalRankingExpressions.addAll(informationalRankingExpressions);
+            return this;
+        }
+
+        /**
          * Sets an optional log tag to indicate the source of this search.
          *
          * <p>Some AppSearch implementations may log a hash of this tag using statsd. This tag may
@@ -1025,6 +1216,7 @@
          *     used to label the search statsd for performance analysis. It is not the tag we are
          *     using in {@link android.util.Log}. The length of the teg should between 1 and 100.
          */
+        @CanIgnoreReturnValue
         @NonNull
         @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG)
         public Builder setSearchSourceLogTag(@NonNull String searchSourceLogTag) {
@@ -1367,6 +1559,64 @@
         }
 
         /**
+         * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the query
+         * expression and the ranking expression for embedding search.
+         *
+         * @see AppSearchSession#search
+         * @see SearchSpec.Builder#setRankingStrategy(String)
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder addSearchEmbeddings(@NonNull EmbeddingVector... searchEmbeddings) {
+            Objects.requireNonNull(searchEmbeddings);
+            resetIfBuilt();
+            return addSearchEmbeddings(Arrays.asList(searchEmbeddings));
+        }
+
+        /**
+         * Adds an embedding search to {@link SearchSpec} Entry, which will be referred in the query
+         * expression and the ranking expression for embedding search.
+         *
+         * @see AppSearchSession#search
+         * @see SearchSpec.Builder#setRankingStrategy(String)
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder addSearchEmbeddings(@NonNull Collection<EmbeddingVector> searchEmbeddings) {
+            Objects.requireNonNull(searchEmbeddings);
+            resetIfBuilt();
+            mSearchEmbeddings.addAll(searchEmbeddings);
+            return this;
+        }
+
+        /**
+         * Sets the default embedding metric type used for embedding search (see {@link
+         * AppSearchSession#search}) and ranking (see {@link
+         * SearchSpec.Builder#setRankingStrategy(String)}).
+         *
+         * <p>If this method is not called, the default embedding search metric type is {@link
+         * SearchSpec#EMBEDDING_SEARCH_METRIC_TYPE_COSINE}. Metrics specified within
+         * "semanticSearch" or "matchedSemanticScores" functions in search/ranking expressions will
+         * override this default.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder setDefaultEmbeddingSearchMetricType(
+                @EmbeddingSearchMetricType int defaultEmbeddingSearchMetricType) {
+            Preconditions.checkArgumentInRange(
+                    defaultEmbeddingSearchMetricType,
+                    EMBEDDING_SEARCH_METRIC_TYPE_COSINE,
+                    EMBEDDING_SEARCH_METRIC_TYPE_EUCLIDEAN,
+                    "Embedding search metric type");
+            resetIfBuilt();
+            mDefaultEmbeddingSearchMetricType = defaultEmbeddingSearchMetricType;
+            return this;
+        }
+
+        /**
          * Sets the NUMERIC_SEARCH feature as enabled/disabled according to the enabled parameter.
          *
          * @param enabled Enables the feature if true, otherwise disables it.
@@ -1374,6 +1624,7 @@
          *     AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric
          *     querying features.
          */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setNumericSearchEnabled(boolean enabled) {
             modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled);
@@ -1391,6 +1642,7 @@
          *     <p>For example, The verbatim string operator '"foo/bar" OR baz' will ensure that
          *     'foo/bar' is treated as a single 'verbatim' token.
          */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setVerbatimSearchEnabled(boolean enabled) {
             modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled);
@@ -1412,7 +1664,7 @@
          *     <p>The newly added custom functions covered by this feature are:
          *     <ul>
          *       <li>createList(String...)
-         *       <li>termSearch(String, List<String>)
+         *       <li>termSearch(String, {@code List<String>})
          *     </ul>
          *     <p>createList takes a variable number of strings and returns a list of strings. It is
          *     for use with termSearch.
@@ -1422,6 +1674,7 @@
          *     example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)" could be
          *     rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))"
          */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setListFilterQueryLanguageEnabled(boolean enabled) {
             modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled);
@@ -1436,6 +1689,7 @@
          *     <p>If disabled, disallows the use of the "hasProperty" function. See {@link
          *     AppSearchSession#search} for more details about the function.
          */
+        @CanIgnoreReturnValue
         @NonNull
         @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION)
         public Builder setListFilterHasPropertyFunctionEnabled(boolean enabled) {
@@ -1444,6 +1698,38 @@
         }
 
         /**
+         * Sets the embedding search feature as enabled/disabled according to the enabled parameter.
+         *
+         * <p>If disabled, disallows the use of the "semanticSearch" function. See {@link
+         * AppSearchSession#search} for more details about the function.
+         *
+         * @param enabled Enables the feature if true, otherwise disables it
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+        public Builder setEmbeddingSearchEnabled(boolean enabled) {
+            modifyEnabledFeature(FeatureConstants.EMBEDDING_SEARCH, enabled);
+            return this;
+        }
+
+        /**
+         * Sets the LIST_FILTER_TOKENIZE_FUNCTION feature as enabled/disabled according to the
+         * enabled parameter.
+         *
+         * @param enabled Enables the feature if true, otherwise disables it
+         *     <p>If disabled, disallows the use of the "tokenize" function. See {@link
+         *     AppSearchSession#search} for more details about the function.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+        public Builder setListFilterTokenizeFunctionEnabled(boolean enabled) {
+            modifyEnabledFeature(FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION, enabled);
+            return this;
+        }
+
+        /**
          * Constructs a new {@link SearchSpec} from the contents of this builder.
          *
          * @throws IllegalArgumentException if property weights are provided with a ranking strategy
@@ -1472,40 +1758,14 @@
                                 + "no JoinSpec provided");
             }
             if (!mTypePropertyWeights.isEmpty()
-                    && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy
-                    && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) {
+                    && mRankingStrategy != RANKING_STRATEGY_RELEVANCE_SCORE
+                    && mRankingStrategy != RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION) {
                 throw new IllegalArgumentException(
                         "Property weights are only compatible with the"
                             + " RANKING_STRATEGY_RELEVANCE_SCORE and"
                             + " RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies.");
             }
 
-            // If the schema filter isn't empty, and there is a schema with a projection but not
-            // in the filter, that is a SearchSpec user error.
-            if (!mSchemas.isEmpty()) {
-                for (String schema : mProjectionTypePropertyMasks.keySet()) {
-                    if (!mSchemas.contains(schema)) {
-                        throw new IllegalArgumentException(
-                                "Projection requested for schema not "
-                                        + "in schemas filters: "
-                                        + schema);
-                    }
-                }
-            }
-
-            Set<String> schemaFilter = new ArraySet<>(mSchemas);
-            if (!mSchemas.isEmpty()) {
-                for (String schema : mTypePropertyFilters.keySet()) {
-                    if (!schemaFilter.contains(schema)) {
-                        throw new IllegalStateException(
-                                "The schema: "
-                                        + schema
-                                        + " exists in the property filter but "
-                                        + "doesn't exist in the schema filter.");
-                    }
-                }
-            }
-
             mBuilt = true;
             return new SearchSpec(
                     mTermMatchType,
@@ -1526,7 +1786,10 @@
                     mJoinSpec,
                     mAdvancedRankingExpression,
                     new ArrayList<>(mEnabledFeatures),
-                    mSearchSourceLogTag);
+                    mSearchSourceLogTag,
+                    mSearchEmbeddings,
+                    mDefaultEmbeddingSearchMetricType,
+                    mInformationalRankingExpressions);
         }
 
         private void resetIfBuilt() {
@@ -1537,6 +1800,9 @@
                 mPackageNames = new ArrayList<>(mPackageNames);
                 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
                 mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights);
+                mSearchEmbeddings = new ArrayList<>(mSearchEmbeddings);
+                mInformationalRankingExpressions =
+                        new ArrayList<>(mInformationalRankingExpressions);
                 mBuilt = false;
             }
         }
diff --git a/framework/java/external/android/app/appsearch/SearchSuggestionResult.java b/framework/java/external/android/app/appsearch/SearchSuggestionResult.java
index c8748f8..27f538d 100644
--- a/framework/java/external/android/app/appsearch/SearchSuggestionResult.java
+++ b/framework/java/external/android/app/appsearch/SearchSuggestionResult.java
@@ -20,18 +20,19 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.util.Objects;
 
 /** The result class of the {@link AppSearchSession#searchSuggestion}. */
 @SafeParcelable.Class(creator = "SearchSuggestionResultCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class SearchSuggestionResult extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
diff --git a/framework/java/external/android/app/appsearch/SearchSuggestionSpec.java b/framework/java/external/android/app/appsearch/SearchSuggestionSpec.java
index e5b774e..5c6d747 100644
--- a/framework/java/external/android/app/appsearch/SearchSuggestionSpec.java
+++ b/framework/java/external/android/app/appsearch/SearchSuggestionSpec.java
@@ -22,7 +22,6 @@
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.app.appsearch.util.BundleUtil;
@@ -32,6 +31,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -52,6 +52,7 @@
  * @see AppSearchSession#searchSuggestion
  */
 @SafeParcelable.Class(creator = "SearchSuggestionSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class SearchSuggestionSpec extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -64,10 +65,12 @@
 
     @Field(id = 2, getter = "getFilterSchemas")
     private final List<String> mFilterSchemas;
+
     // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is
     // schema type and value is a list of target property paths in that schema to search over.
     @Field(id = 3)
     final Bundle mFilterProperties;
+
     // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is
     // namespace and value is a list of target document ids in that namespace to search over.
     @Field(id = 4)
@@ -126,6 +129,7 @@
      * score and appear in the results first.
      */
     public static final int SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT = 0;
+
     /**
      * Ranked by the term appear frequency.
      *
@@ -351,6 +355,7 @@
          *     of property names indicating which property in the document these snippets correspond
          *     to.
          */
+        @CanIgnoreReturnValue
         @NonNull
         @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public Builder addFilterProperties(
@@ -382,6 +387,7 @@
          * @param schema the {@link AppSearchSchema} that contains the target properties
          * @param propertyPaths The {@link PropertyPath} to search suggestion over
          */
+        @CanIgnoreReturnValue
         @NonNull
         // Getter method is getFilterProperties
         @SuppressLint("MissingGetterMatchingBuilder")
diff --git a/framework/java/external/android/app/appsearch/SetSchemaRequest.java b/framework/java/external/android/app/appsearch/SetSchemaRequest.java
index 02d3689..4c14e34 100644
--- a/framework/java/external/android/app/appsearch/SetSchemaRequest.java
+++ b/framework/java/external/android/app/appsearch/SetSchemaRequest.java
@@ -23,10 +23,10 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -549,6 +549,7 @@
          *     schema}.
          */
         // Merged list available from getPubliclyVisibleSchemas
+        @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
         @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
         @NonNull
@@ -583,6 +584,7 @@
          *     that a call must to match to access the schema.
          */
         // Merged list available from getSchemasVisibleToConfigs
+        @CanIgnoreReturnValue
         @SuppressLint("MissingGetterMatchingBuilder")
         @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
         @NonNull
@@ -602,6 +604,7 @@
         }
 
         /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */
+        @CanIgnoreReturnValue
         @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS)
         @NonNull
         public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) {
diff --git a/framework/java/external/android/app/appsearch/SetSchemaResponse.java b/framework/java/external/android/app/appsearch/SetSchemaResponse.java
index a588cc9..a00b0b5 100644
--- a/framework/java/external/android/app/appsearch/SetSchemaResponse.java
+++ b/framework/java/external/android/app/appsearch/SetSchemaResponse.java
@@ -20,13 +20,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.util.ArrayList;
@@ -38,6 +38,7 @@
 
 /** The response class of {@link AppSearchSession#setSchema} */
 @SafeParcelable.Class(creator = "SetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class SetSchemaResponse extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -316,6 +317,7 @@
      * {@link AppSearchSession#setSchema}.
      */
     @SafeParcelable.Class(creator = "MigrationFailureCreator")
+    @SuppressWarnings("HiddenSuperclass")
     public static class MigrationFailure extends AbstractSafeParcelable {
 
         @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
@@ -333,6 +335,7 @@
         private final String mSchemaType;
 
         @Field(id = 4)
+        @Nullable
         final String mErrorMessage;
 
         @Field(id = 5)
@@ -343,7 +346,7 @@
                 @Param(id = 1) @NonNull String namespace,
                 @Param(id = 2) @NonNull String documentId,
                 @Param(id = 3) @NonNull String schemaType,
-                @Param(id = 4) @NonNull String errorMessage,
+                @Param(id = 4) @Nullable String errorMessage,
                 @Param(id = 5) int resultCode) {
             mNamespace = namespace;
             mDocumentId = documentId;
diff --git a/framework/java/external/android/app/appsearch/StorageInfo.java b/framework/java/external/android/app/appsearch/StorageInfo.java
index 5b7b5a5..d5888e2 100644
--- a/framework/java/external/android/app/appsearch/StorageInfo.java
+++ b/framework/java/external/android/app/appsearch/StorageInfo.java
@@ -19,14 +19,16 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.appsearch.flags.Flags;
+
 /** The response class of {@code AppSearchSession#getStorageInfo}. */
 @SafeParcelable.Class(creator = "StorageInfoCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class StorageInfo extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
diff --git a/framework/java/external/android/app/appsearch/VisibilityPermissionConfig.java b/framework/java/external/android/app/appsearch/VisibilityPermissionConfig.java
index d9e51b2..0bf8109 100644
--- a/framework/java/external/android/app/appsearch/VisibilityPermissionConfig.java
+++ b/framework/java/external/android/app/appsearch/VisibilityPermissionConfig.java
@@ -21,6 +21,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArraySet;
 
 import java.util.Arrays;
@@ -36,7 +37,7 @@
 @SafeParcelable.Class(creator = "VisibilityPermissionConfigCreator")
 public final class VisibilityPermissionConfig extends AbstractSafeParcelable {
     @NonNull
-    public static final VisibilityPermissionConfigCreator CREATOR =
+    public static final Parcelable.Creator<VisibilityPermissionConfig> CREATOR =
             new VisibilityPermissionConfigCreator();
 
     /**
@@ -131,7 +132,7 @@
         if (mGenericDocument == null) {
             // This is used as a nested document, we do not need a namespace or id.
             GenericDocument.Builder<?> builder =
-                    new GenericDocument.Builder<>(/*namespace=*/ "", /*id=*/ "", SCHEMA_TYPE);
+                    new GenericDocument.Builder<>(/* namespace= */ "", /* id= */ "", SCHEMA_TYPE);
 
             if (mAllRequiredPermissions != null) {
                 // GenericDocument only supports long, so int[] needs to be converted to
diff --git a/framework/java/external/android/app/appsearch/checker/initialization/qual/UnderInitialization.java b/framework/java/external/android/app/appsearch/checker/initialization/qual/UnderInitialization.java
new file mode 100644
index 0000000..277d045
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/checker/initialization/qual/UnderInitialization.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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.checker.initialization.qual;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/** @hide */
+@Target({
+    ElementType.ANNOTATION_TYPE,
+    ElementType.CONSTRUCTOR,
+    ElementType.FIELD,
+    ElementType.LOCAL_VARIABLE,
+    ElementType.METHOD,
+    ElementType.PACKAGE,
+    ElementType.PARAMETER,
+    ElementType.TYPE,
+    ElementType.TYPE_PARAMETER,
+    ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface UnderInitialization {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/framework/java/external/android/app/appsearch/checker/initialization/qual/UnknownInitialization.java b/framework/java/external/android/app/appsearch/checker/initialization/qual/UnknownInitialization.java
new file mode 100644
index 0000000..6d0fb87
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/checker/initialization/qual/UnknownInitialization.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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.checker.initialization.qual;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/** @hide */
+@Target({
+    ElementType.ANNOTATION_TYPE,
+    ElementType.CONSTRUCTOR,
+    ElementType.FIELD,
+    ElementType.LOCAL_VARIABLE,
+    ElementType.METHOD,
+    ElementType.PACKAGE,
+    ElementType.PARAMETER,
+    ElementType.TYPE,
+    ElementType.TYPE_PARAMETER,
+    ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface UnknownInitialization {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/framework/java/external/android/app/appsearch/checker/nullness/qual/Nullable.java b/framework/java/external/android/app/appsearch/checker/nullness/qual/Nullable.java
new file mode 100644
index 0000000..d832dcc
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/checker/nullness/qual/Nullable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 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.checker.nullness.qual;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** @hide */
+@Target({
+    ElementType.ANNOTATION_TYPE,
+    ElementType.CONSTRUCTOR,
+    ElementType.FIELD,
+    ElementType.LOCAL_VARIABLE,
+    ElementType.METHOD,
+    ElementType.PACKAGE,
+    ElementType.PARAMETER,
+    ElementType.TYPE,
+    ElementType.TYPE_PARAMETER,
+    ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface Nullable {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/framework/java/external/android/app/appsearch/checker/nullness/qual/RequiresNonNull.java b/framework/java/external/android/app/appsearch/checker/nullness/qual/RequiresNonNull.java
new file mode 100644
index 0000000..087713b
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/checker/nullness/qual/RequiresNonNull.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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.checker.nullness.qual;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// This is an annotation stub to avoid dependencies on annotations that aren't
+// in the Android platform source tree.
+
+/** @hide */
+@Target({
+    ElementType.ANNOTATION_TYPE,
+    ElementType.CONSTRUCTOR,
+    ElementType.FIELD,
+    ElementType.LOCAL_VARIABLE,
+    ElementType.METHOD,
+    ElementType.PACKAGE,
+    ElementType.PARAMETER,
+    ElementType.TYPE,
+    ElementType.TYPE_PARAMETER,
+    ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface RequiresNonNull {
+
+    // These fields maintain API compatibility with annotations that expect arguments.
+
+    String[] value() default {};
+
+    boolean result() default false;
+
+    String[] expression() default "";
+}
diff --git a/framework/java/external/android/app/appsearch/exceptions/AppSearchException.java b/framework/java/external/android/app/appsearch/exceptions/AppSearchException.java
index dad59a9..a7db12c 100644
--- a/framework/java/external/android/app/appsearch/exceptions/AppSearchException.java
+++ b/framework/java/external/android/app/appsearch/exceptions/AppSearchException.java
@@ -27,7 +27,7 @@
  * client.
  */
 public class AppSearchException extends Exception {
-    private final @AppSearchResult.ResultCode int mResultCode;
+    @AppSearchResult.ResultCode private final int mResultCode;
 
     /**
      * Initializes an {@link AppSearchException} with no message.
@@ -35,7 +35,7 @@
      * @param resultCode One of the constants documented in {@link AppSearchResult#getResultCode}.
      */
     public AppSearchException(@AppSearchResult.ResultCode int resultCode) {
-        this(resultCode, /*message=*/ null);
+        this(resultCode, /* message= */ null);
     }
 
     /**
@@ -47,7 +47,7 @@
      */
     public AppSearchException(
             @AppSearchResult.ResultCode int resultCode, @Nullable String message) {
-        this(resultCode, message, /*cause=*/ null);
+        this(resultCode, message, /* cause= */ null);
     }
 
     /**
diff --git a/framework/java/external/android/app/appsearch/flags/Flags.java b/framework/java/external/android/app/appsearch/flags/Flags.java
deleted file mode 100644
index 9217e44..0000000
--- a/framework/java/external/android/app/appsearch/flags/Flags.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2023 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.flags;
-
-
-/**
- * Flags to control different features.
- *
- * <p>In Jetpack, those values can't be changed during runtime.
- *
- * @hide
- */
-public final class Flags {
-    private Flags() {}
-
-    // The prefix of all the flags defined for AppSearch. The prefix has
-    // "com.android.appsearch.flags", aka the package name for generated AppSearch flag classes in
-    // the framework, plus an additional trailing '.'.
-    private static final String FLAG_PREFIX = "com.android.appsearch.flags.";
-
-    // The full string values for flags defined in the framework.
-    //
-    // The values of the static variables are the names of the flag defined in the framework's
-    // aconfig files. E.g. "enable_safe_parcelable", with FLAG_PREFIX as the prefix.
-    //
-    // The name of the each static variable should be "FLAG_" + capitalized value of the flag.
-
-    /** Enable SafeParcelable related features. */
-    public static final String FLAG_ENABLE_SAFE_PARCELABLE_2 =
-            FLAG_PREFIX + "enable_safe_parcelable_2";
-
-    /** Enable the "hasProperty" function in list filter query expressions. */
-    public static final String FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION =
-            FLAG_PREFIX + "enable_list_filter_has_property_function";
-
-    /** Enable Schema Type Grouping related features. */
-    public static final String FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA =
-            FLAG_PREFIX + "enable_grouping_type_per_schema";
-
-    /** Enable GenericDocument to take another GenericDocument to copy construct. */
-    public static final String FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR =
-            FLAG_PREFIX + "enable_generic_document_copy_constructor";
-
-    /**
-     * Enable the {@link android.app.appsearch.SearchSpec.Builder#addFilterProperties} and {@link
-     * android.app.appsearch.SearchSuggestionSpec.Builder#addFilterProperties}.
-     */
-    public static final String FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES =
-            FLAG_PREFIX + "enable_search_spec_filter_properties";
-    /** Enable the {@link android.app.appsearch.SearchSpec.Builder#setSearchSourceLogTag} method. */
-    public static final String FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG =
-            FLAG_PREFIX + "enable_search_spec_set_search_source_log_tag";
-
-    /** Enable addTakenActions API in PutDocumentsRequest. */
-    public static final String FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS =
-            FLAG_PREFIX + "enable_put_documents_request_add_taken_actions";
-
-    /** Enable setPubliclyVisibleSchema in SetSchemaRequest. */
-    public static final String FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA =
-            FLAG_PREFIX + "enable_set_publicly_visible_schema";
-
-    /**
-     * Enable {@link android.app.appsearch.GenericDocument.Builder} to use previously hidden
-     * methods.
-     */
-    public static final String FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS =
-            FLAG_PREFIX + "enable_generic_document_builder_hidden_methods";
-
-    public static final String FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS =
-            FLAG_PREFIX + "enable_set_schema_visible_to_configs";
-
-    /** Enable {@link android.app.appsearch.EnterpriseGlobalSearchSession}. */
-    public static final String FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION =
-            FLAG_PREFIX + "enable_enterprise_global_search_session";
-
-    // Whether the features should be enabled.
-    //
-    // In Jetpack, those should always return true.
-
-    /** Whether SafeParcelable should be enabled. */
-    public static boolean enableSafeParcelable() {
-        return true;
-    }
-
-    /** Whether the "hasProperty" function in list filter query expressions should be enabled. */
-    public static boolean enableListFilterHasPropertyFunction() {
-        return true;
-    }
-
-    /** Whether Schema Type Grouping should be enabled. */
-    public static boolean enableGroupingTypePerSchema() {
-        return true;
-    }
-
-    /** Whether Generic Document Copy Constructing should be enabled. */
-    public static boolean enableGenericDocumentCopyConstructor() {
-        return true;
-    }
-
-    /**
-     * Whether the {@link android.app.appsearch.SearchSpec.Builder#addFilterProperties} and {@link
-     * android.app.appsearch.SearchSuggestionSpec.Builder#addFilterProperties} should be enabled.
-     */
-    public static boolean enableSearchSpecFilterProperties() {
-        return true;
-    }
-
-    /**
-     * Whether the {@link android.app.appsearch.SearchSpec.Builder#setSearchSourceLogTag} should be
-     * enabled.
-     */
-    public static boolean enableSearchSpecSetSearchSourceLogTag() {
-        return true;
-    }
-
-    /** Whether addTakenActions API in PutDocumentsRequest should be enabled. */
-    public static boolean enablePutDocumentsRequestAddTakenActions() {
-        return true;
-    }
-
-    /** Whether setPubliclyVisibleSchema in SetSchemaRequest.Builder should be enabled. */
-    public static boolean enableSetPubliclyVisibleSchema() {
-        return true;
-    }
-
-    /**
-     * Whether {@link android.app.appsearch.GenericDocument.Builder#setNamespace(String)}, {@link
-     * android.app.appsearch.GenericDocument.Builder#setId(String)}, {@link
-     * android.app.appsearch.GenericDocument.Builder#setSchemaType(String)}, and {@link
-     * android.app.appsearch.GenericDocument.Builder#clearProperty(String)} should be enabled.
-     */
-    public static boolean enableGenericDocumentBuilderHiddenMethods() {
-        return true;
-    }
-
-    /**
-     * Whether {@link android.app.appsearch.SetSchemaRequest.Builder
-     * #setSchemaTypeVisibilityForConfigs} should be enabled.
-     */
-    public static boolean enableSetSchemaVisibleToConfigs() {
-        return true;
-    }
-
-    /** Whether {@link android.app.appsearch.EnterpriseGlobalSearchSession} should be enabled. */
-    public static boolean enableEnterpriseGlobalSearchSession() {
-        return true;
-    }
-}
diff --git a/framework/java/external/android/app/appsearch/observer/ObserverSpec.java b/framework/java/external/android/app/appsearch/observer/ObserverSpec.java
index 60caa2e..e81beb8 100644
--- a/framework/java/external/android/app/appsearch/observer/ObserverSpec.java
+++ b/framework/java/external/android/app/appsearch/observer/ObserverSpec.java
@@ -20,13 +20,14 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.flags.Flags;
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArraySet;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -40,6 +41,7 @@
  * match against.
  */
 @SafeParcelable.Class(creator = "ObserverSpecCreator")
+@SuppressWarnings("HiddenSuperclass")
 public final class ObserverSpec extends AbstractSafeParcelable {
 
     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
diff --git a/framework/java/external/android/app/appsearch/safeparcel/GenericDocumentParcel.java b/framework/java/external/android/app/appsearch/safeparcel/GenericDocumentParcel.java
index d6b786d..c77aa96 100644
--- a/framework/java/external/android/app/appsearch/safeparcel/GenericDocumentParcel.java
+++ b/framework/java/external/android/app/appsearch/safeparcel/GenericDocumentParcel.java
@@ -22,6 +22,7 @@
 import android.annotation.SuppressLint;
 import android.app.appsearch.AppSearchSchema;
 import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.EmbeddingVector;
 import android.app.appsearch.GenericDocument;
 import android.app.appsearch.annotation.CanIgnoreReturnValue;
 import android.os.Parcel;
@@ -44,7 +45,8 @@
 @SuppressLint("BanParcelableUsage")
 public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable {
     @NonNull
-    public static final GenericDocumentParcelCreator CREATOR = new GenericDocumentParcelCreator();
+    public static final Parcelable.Creator<GenericDocumentParcel> CREATOR =
+            new GenericDocumentParcelCreator();
 
     /** The default score of document. */
     private static final int DEFAULT_SCORE = 0;
@@ -154,6 +156,14 @@
         mParentTypes = parentTypes;
     }
 
+    /** Returns the {@link GenericDocumentParcel} object from the given {@link GenericDocument}. */
+    @NonNull
+    public static GenericDocumentParcel fromGenericDocument(
+            @NonNull GenericDocument genericDocument) {
+        Objects.requireNonNull(genericDocument);
+        return genericDocument.getDocumentParcel();
+    }
+
     private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
             @NonNull List<PropertyParcel> properties) {
         Objects.requireNonNull(properties);
@@ -275,10 +285,10 @@
         private long mTtlMillis;
         private int mScore;
         private Map<String, PropertyParcel> mPropertyMap;
-        private List<String> mParentTypes;
+        @Nullable private List<String> mParentTypes;
 
         /**
-         * Creates a new {@link GenericDocument.Builder}.
+         * Creates a new {@link GenericDocumentParcel.Builder}.
          *
          * <p>Document IDs are unique within a namespace.
          *
@@ -442,6 +452,7 @@
         }
 
         /** puts an array of {@link String} in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values)
                 throws IllegalArgumentException {
@@ -451,6 +462,7 @@
         }
 
         /** puts an array of boolean in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {
             putInPropertyMap(
@@ -459,6 +471,7 @@
         }
 
         /** puts an array of double in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {
             putInPropertyMap(
@@ -467,6 +480,7 @@
         }
 
         /** puts an array of long in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {
             putInPropertyMap(name, new PropertyParcel.Builder(name).setLongValues(values).build());
@@ -474,6 +488,7 @@
         }
 
         /** Converts and saves a byte[][] into {@link #mProperties}. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {
             putInPropertyMap(name, new PropertyParcel.Builder(name).setBytesValues(values).build());
@@ -481,6 +496,7 @@
         }
 
         /** puts an array of {@link GenericDocumentParcel} in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(
                 @NonNull String name, @NonNull GenericDocumentParcel[] values) {
@@ -489,7 +505,17 @@
             return this;
         }
 
+        /** puts an array of {@link EmbeddingVector} in property map. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder putInPropertyMap(@NonNull String name, @NonNull EmbeddingVector[] values) {
+            putInPropertyMap(
+                    name, new PropertyParcel.Builder(name).setEmbeddingValues(values).build());
+            return this;
+        }
+
         /** Directly puts a {@link PropertyParcel} in property map. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder putInPropertyMap(@NonNull String name, @NonNull PropertyParcel value) {
             Objects.requireNonNull(value);
diff --git a/framework/java/external/android/app/appsearch/safeparcel/PackageIdentifierParcel.java b/framework/java/external/android/app/appsearch/safeparcel/PackageIdentifierParcel.java
index 1e7ed3a..d97e8bc 100644
--- a/framework/java/external/android/app/appsearch/safeparcel/PackageIdentifierParcel.java
+++ b/framework/java/external/android/app/appsearch/safeparcel/PackageIdentifierParcel.java
@@ -21,10 +21,11 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.appsearch.PackageIdentifier;
-import android.app.appsearch.flags.Flags;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.appsearch.flags.Flags;
+
 import java.util.Arrays;
 import java.util.Objects;
 
diff --git a/framework/java/external/android/app/appsearch/safeparcel/PropertyConfigParcel.java b/framework/java/external/android/app/appsearch/safeparcel/PropertyConfigParcel.java
index 02a5141..128010c 100644
--- a/framework/java/external/android/app/appsearch/safeparcel/PropertyConfigParcel.java
+++ b/framework/java/external/android/app/appsearch/safeparcel/PropertyConfigParcel.java
@@ -24,6 +24,7 @@
 import android.app.appsearch.AppSearchSchema.StringPropertyConfig.JoinableValueType;
 import android.app.appsearch.AppSearchSchema.StringPropertyConfig.TokenizerType;
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import java.util.List;
 import java.util.Objects;
@@ -41,7 +42,8 @@
 @SafeParcelable.Class(creator = "PropertyConfigParcelCreator")
 public final class PropertyConfigParcel extends AbstractSafeParcelable {
     @NonNull
-    public static final PropertyConfigParcelCreator CREATOR = new PropertyConfigParcelCreator();
+    public static final Parcelable.Creator<PropertyConfigParcel> CREATOR =
+            new PropertyConfigParcelCreator();
 
     @Field(id = 1, getter = "getName")
     private final String mName;
@@ -55,20 +57,31 @@
     private final int mCardinality;
 
     @Field(id = 4, getter = "getSchemaType")
+    @Nullable
     private final String mSchemaType;
 
     @Field(id = 5, getter = "getStringIndexingConfigParcel")
+    @Nullable
     private final StringIndexingConfigParcel mStringIndexingConfigParcel;
 
     @Field(id = 6, getter = "getDocumentIndexingConfigParcel")
+    @Nullable
     private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
 
     @Field(id = 7, getter = "getIntegerIndexingConfigParcel")
+    @Nullable
     private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
 
     @Field(id = 8, getter = "getJoinableConfigParcel")
+    @Nullable
     private final JoinableConfigParcel mJoinableConfigParcel;
 
+    @Field(id = 9, getter = "getDescription")
+    private final String mDescription;
+
+    @Field(id = 10, getter = "getEmbeddingIndexingConfigParcel")
+    private final EmbeddingIndexingConfigParcel mEmbeddingIndexingConfigParcel;
+
     @Nullable private Integer mHashCode;
 
     /** Constructor for {@link PropertyConfigParcel}. */
@@ -81,7 +94,9 @@
             @Param(id = 5) @Nullable StringIndexingConfigParcel stringIndexingConfigParcel,
             @Param(id = 6) @Nullable DocumentIndexingConfigParcel documentIndexingConfigParcel,
             @Param(id = 7) @Nullable IntegerIndexingConfigParcel integerIndexingConfigParcel,
-            @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel) {
+            @Param(id = 8) @Nullable JoinableConfigParcel joinableConfigParcel,
+            @Param(id = 9) @NonNull String description,
+            @Param(id = 10) @Nullable EmbeddingIndexingConfigParcel embeddingIndexingConfigParcel) {
         mName = Objects.requireNonNull(name);
         mDataType = dataType;
         mCardinality = cardinality;
@@ -90,12 +105,15 @@
         mDocumentIndexingConfigParcel = documentIndexingConfigParcel;
         mIntegerIndexingConfigParcel = integerIndexingConfigParcel;
         mJoinableConfigParcel = joinableConfigParcel;
+        mDescription = Objects.requireNonNull(description);
+        mEmbeddingIndexingConfigParcel = embeddingIndexingConfigParcel;
     }
 
     /** Creates a {@link PropertyConfigParcel} for String. */
     @NonNull
     public static PropertyConfigParcel createForString(
             @NonNull String propertyName,
+            @NonNull String description,
             @Cardinality int cardinality,
             @NonNull StringIndexingConfigParcel stringIndexingConfigParcel,
             @NonNull JoinableConfigParcel joinableConfigParcel) {
@@ -103,79 +121,97 @@
                 Objects.requireNonNull(propertyName),
                 AppSearchSchema.PropertyConfig.DATA_TYPE_STRING,
                 cardinality,
-                /*schemaType=*/ null,
+                /* schemaType= */ null,
                 Objects.requireNonNull(stringIndexingConfigParcel),
-                /*documentIndexingConfigParcel=*/ null,
-                /*integerIndexingConfigParcel=*/ null,
-                Objects.requireNonNull(joinableConfigParcel));
+                /* documentIndexingConfigParcel= */ null,
+                /* integerIndexingConfigParcel= */ null,
+                Objects.requireNonNull(joinableConfigParcel),
+                Objects.requireNonNull(description),
+                /* embeddingIndexingConfigParcel= */ null);
     }
 
     /** Creates a {@link PropertyConfigParcel} for Long. */
     @NonNull
     public static PropertyConfigParcel createForLong(
             @NonNull String propertyName,
+            @NonNull String description,
             @Cardinality int cardinality,
             @AppSearchSchema.LongPropertyConfig.IndexingType int indexingType) {
         return new PropertyConfigParcel(
                 Objects.requireNonNull(propertyName),
                 AppSearchSchema.PropertyConfig.DATA_TYPE_LONG,
                 cardinality,
-                /*schemaType=*/ null,
-                /*stringIndexingConfigParcel=*/ null,
-                /*documentIndexingConfigParcel=*/ null,
+                /* schemaType= */ null,
+                /* stringIndexingConfigParcel= */ null,
+                /* documentIndexingConfigParcel= */ null,
                 new IntegerIndexingConfigParcel(indexingType),
-                /*joinableConfigParcel=*/ null);
+                /* joinableConfigParcel= */ null,
+                Objects.requireNonNull(description),
+                /* embeddingIndexingConfigParcel= */ null);
     }
 
     /** Creates a {@link PropertyConfigParcel} for Double. */
     @NonNull
     public static PropertyConfigParcel createForDouble(
-            @NonNull String propertyName, @Cardinality int cardinality) {
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality) {
         return new PropertyConfigParcel(
                 Objects.requireNonNull(propertyName),
                 AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE,
                 cardinality,
-                /*schemaType=*/ null,
-                /*stringIndexingConfigParcel=*/ null,
-                /*documentIndexingConfigParcel=*/ null,
-                /*integerIndexingConfigParcel=*/ null,
-                /*joinableConfigParcel=*/ null);
+                /* schemaType= */ null,
+                /* stringIndexingConfigParcel= */ null,
+                /* documentIndexingConfigParcel= */ null,
+                /* integerIndexingConfigParcel= */ null,
+                /* joinableConfigParcel= */ null,
+                Objects.requireNonNull(description),
+                /* embeddingIndexingConfigParcel= */ null);
     }
 
     /** Creates a {@link PropertyConfigParcel} for Boolean. */
     @NonNull
     public static PropertyConfigParcel createForBoolean(
-            @NonNull String propertyName, @Cardinality int cardinality) {
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality) {
         return new PropertyConfigParcel(
                 Objects.requireNonNull(propertyName),
                 AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN,
                 cardinality,
-                /*schemaType=*/ null,
-                /*stringIndexingConfigParcel=*/ null,
-                /*documentIndexingConfigParcel=*/ null,
-                /*integerIndexingConfigParcel=*/ null,
-                /*joinableConfigParcel=*/ null);
+                /* schemaType= */ null,
+                /* stringIndexingConfigParcel= */ null,
+                /* documentIndexingConfigParcel= */ null,
+                /* integerIndexingConfigParcel= */ null,
+                /* joinableConfigParcel= */ null,
+                Objects.requireNonNull(description),
+                /* embeddingIndexingConfigParcel= */ null);
     }
 
     /** Creates a {@link PropertyConfigParcel} for Bytes. */
     @NonNull
     public static PropertyConfigParcel createForBytes(
-            @NonNull String propertyName, @Cardinality int cardinality) {
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality) {
         return new PropertyConfigParcel(
                 Objects.requireNonNull(propertyName),
                 AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES,
                 cardinality,
-                /*schemaType=*/ null,
-                /*stringIndexingConfigParcel=*/ null,
-                /*documentIndexingConfigParcel=*/ null,
-                /*integerIndexingConfigParcel=*/ null,
-                /*joinableConfigParcel=*/ null);
+                /* schemaType= */ null,
+                /* stringIndexingConfigParcel= */ null,
+                /* documentIndexingConfigParcel= */ null,
+                /* integerIndexingConfigParcel= */ null,
+                /* joinableConfigParcel= */ null,
+                Objects.requireNonNull(description),
+                /* embeddingIndexingConfigParcel= */ null);
     }
 
     /** Creates a {@link PropertyConfigParcel} for Document. */
     @NonNull
     public static PropertyConfigParcel createForDocument(
             @NonNull String propertyName,
+            @NonNull String description,
             @Cardinality int cardinality,
             @NonNull String schemaType,
             @NonNull DocumentIndexingConfigParcel documentIndexingConfigParcel) {
@@ -184,10 +220,32 @@
                 AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT,
                 cardinality,
                 Objects.requireNonNull(schemaType),
-                /*stringIndexingConfigParcel=*/ null,
+                /* stringIndexingConfigParcel= */ null,
                 Objects.requireNonNull(documentIndexingConfigParcel),
-                /*integerIndexingConfigParcel=*/ null,
-                /*joinableConfigParcel=*/ null);
+                /* integerIndexingConfigParcel= */ null,
+                /* joinableConfigParcel= */ null,
+                Objects.requireNonNull(description),
+                /* embeddingIndexingConfigParcel= */ null);
+    }
+
+    /** Creates a {@link PropertyConfigParcel} for Embedding. */
+    @NonNull
+    public static PropertyConfigParcel createForEmbedding(
+            @NonNull String propertyName,
+            @NonNull String description,
+            @Cardinality int cardinality,
+            @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
+        return new PropertyConfigParcel(
+                Objects.requireNonNull(propertyName),
+                AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING,
+                cardinality,
+                /* schemaType= */ null,
+                /* stringIndexingConfigParcel= */ null,
+                /* documentIndexingConfigParcel= */ null,
+                /* integerIndexingConfigParcel= */ null,
+                /* joinableConfigParcel= */ null,
+                Objects.requireNonNull(description),
+                new EmbeddingIndexingConfigParcel(indexingType));
     }
 
     /** Gets name for the property. */
@@ -196,6 +254,12 @@
         return mName;
     }
 
+    /** Gets description for the property. */
+    @NonNull
+    public String getDescription() {
+        return mDescription;
+    }
+
     /** Gets data type for the property. */
     @DataType
     public int getDataType() {
@@ -238,6 +302,12 @@
         return mJoinableConfigParcel;
     }
 
+    /** Gets the {@link EmbeddingIndexingConfigParcel}. */
+    @Nullable
+    public EmbeddingIndexingConfigParcel getEmbeddingIndexingConfigParcel() {
+        return mEmbeddingIndexingConfigParcel;
+    }
+
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
@@ -253,6 +323,7 @@
         }
         PropertyConfigParcel otherProperty = (PropertyConfigParcel) other;
         return Objects.equals(mName, otherProperty.mName)
+                && Objects.equals(mDescription, otherProperty.mDescription)
                 && Objects.equals(mDataType, otherProperty.mDataType)
                 && Objects.equals(mCardinality, otherProperty.mCardinality)
                 && Objects.equals(mSchemaType, otherProperty.mSchemaType)
@@ -262,7 +333,10 @@
                         mDocumentIndexingConfigParcel, otherProperty.mDocumentIndexingConfigParcel)
                 && Objects.equals(
                         mIntegerIndexingConfigParcel, otherProperty.mIntegerIndexingConfigParcel)
-                && Objects.equals(mJoinableConfigParcel, otherProperty.mJoinableConfigParcel);
+                && Objects.equals(mJoinableConfigParcel, otherProperty.mJoinableConfigParcel)
+                && Objects.equals(
+                        mEmbeddingIndexingConfigParcel,
+                        otherProperty.mEmbeddingIndexingConfigParcel);
     }
 
     @Override
@@ -271,13 +345,15 @@
             mHashCode =
                     Objects.hash(
                             mName,
+                            mDescription,
                             mDataType,
                             mCardinality,
                             mSchemaType,
                             mStringIndexingConfigParcel,
                             mDocumentIndexingConfigParcel,
                             mIntegerIndexingConfigParcel,
-                            mJoinableConfigParcel);
+                            mJoinableConfigParcel,
+                            mEmbeddingIndexingConfigParcel);
         }
         return mHashCode;
     }
@@ -287,6 +363,8 @@
     public String toString() {
         return "{name: "
                 + mName
+                + ", description: "
+                + mDescription
                 + ", dataType: "
                 + mDataType
                 + ", cardinality: "
@@ -301,6 +379,8 @@
                 + mIntegerIndexingConfigParcel
                 + ", joinableConfigParcel: "
                 + mJoinableConfigParcel
+                + ", embeddingIndexingConfigParcel: "
+                + mEmbeddingIndexingConfigParcel
                 + "}";
     }
 
@@ -308,7 +388,8 @@
     @SafeParcelable.Class(creator = "JoinableConfigParcelCreator")
     public static class JoinableConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final JoinableConfigParcelCreator CREATOR = new JoinableConfigParcelCreator();
+        public static final Parcelable.Creator<JoinableConfigParcel> CREATOR =
+                new JoinableConfigParcelCreator();
 
         @JoinableValueType
         @Field(id = 1, getter = "getJoinableValueType")
@@ -375,7 +456,7 @@
     @SafeParcelable.Class(creator = "StringIndexingConfigParcelCreator")
     public static class StringIndexingConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final StringIndexingConfigParcelCreator CREATOR =
+        public static final Parcelable.Creator<StringIndexingConfigParcel> CREATOR =
                 new StringIndexingConfigParcelCreator();
 
         @AppSearchSchema.StringPropertyConfig.IndexingType
@@ -441,7 +522,7 @@
     @SafeParcelable.Class(creator = "IntegerIndexingConfigParcelCreator")
     public static class IntegerIndexingConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final IntegerIndexingConfigParcelCreator CREATOR =
+        public static final Parcelable.Creator<IntegerIndexingConfigParcel> CREATOR =
                 new IntegerIndexingConfigParcelCreator();
 
         @AppSearchSchema.LongPropertyConfig.IndexingType
@@ -468,7 +549,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(mIndexingType);
+            return Objects.hashCode(mIndexingType);
         }
 
         @Override
@@ -494,7 +575,7 @@
     @SafeParcelable.Class(creator = "DocumentIndexingConfigParcelCreator")
     public static class DocumentIndexingConfigParcel extends AbstractSafeParcelable {
         @NonNull
-        public static final DocumentIndexingConfigParcelCreator CREATOR =
+        public static final Parcelable.Creator<DocumentIndexingConfigParcel> CREATOR =
                 new DocumentIndexingConfigParcelCreator();
 
         @Field(id = 1, getter = "shouldIndexNestedProperties")
@@ -559,4 +640,58 @@
                     + "}";
         }
     }
+
+    /** Class to hold configuration for embedding property. */
+    @SafeParcelable.Class(creator = "EmbeddingIndexingConfigParcelCreator")
+    public static class EmbeddingIndexingConfigParcel extends AbstractSafeParcelable {
+        @NonNull
+        public static final Parcelable.Creator<EmbeddingIndexingConfigParcel> CREATOR =
+                new EmbeddingIndexingConfigParcelCreator();
+
+        @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+        @Field(id = 1, getter = "getIndexingType")
+        private final int mIndexingType;
+
+        /** Constructor for {@link EmbeddingIndexingConfigParcel}. */
+        @Constructor
+        public EmbeddingIndexingConfigParcel(
+                @Param(id = 1) @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+                        int indexingType) {
+            mIndexingType = indexingType;
+        }
+
+        /** Gets the indexing type for this embedding property. */
+        @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+        public int getIndexingType() {
+            return mIndexingType;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            EmbeddingIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(mIndexingType);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof EmbeddingIndexingConfigParcel)) {
+                return false;
+            }
+            EmbeddingIndexingConfigParcel otherObject = (EmbeddingIndexingConfigParcel) other;
+            return Objects.equals(mIndexingType, otherObject.mIndexingType);
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "{indexingType: " + mIndexingType + "}";
+        }
+    }
 }
diff --git a/framework/java/external/android/app/appsearch/safeparcel/PropertyParcel.java b/framework/java/external/android/app/appsearch/safeparcel/PropertyParcel.java
index e3443af..72b994f 100644
--- a/framework/java/external/android/app/appsearch/safeparcel/PropertyParcel.java
+++ b/framework/java/external/android/app/appsearch/safeparcel/PropertyParcel.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.app.appsearch.EmbeddingVector;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -36,7 +38,8 @@
 // This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
 @SuppressLint("BanParcelableUsage")
 public final class PropertyParcel extends AbstractSafeParcelable implements Parcelable {
-    @NonNull public static final PropertyParcelCreator CREATOR = new PropertyParcelCreator();
+    @NonNull
+    public static final Parcelable.Creator<PropertyParcel> CREATOR = new PropertyParcelCreator();
 
     @NonNull
     @Field(id = 1, getter = "getPropertyName")
@@ -66,6 +69,10 @@
     @Field(id = 7, getter = "getDocumentValues")
     private final GenericDocumentParcel[] mDocumentValues;
 
+    @Nullable
+    @Field(id = 8, getter = "getEmbeddingValues")
+    private final EmbeddingVector[] mEmbeddingValues;
+
     @Nullable private Integer mHashCode;
 
     @Constructor
@@ -76,7 +83,8 @@
             @Param(id = 4) @Nullable double[] doubleValues,
             @Param(id = 5) @Nullable boolean[] booleanValues,
             @Param(id = 6) @Nullable byte[][] bytesValues,
-            @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues) {
+            @Param(id = 7) @Nullable GenericDocumentParcel[] documentValues,
+            @Param(id = 8) @Nullable EmbeddingVector[] embeddingValues) {
         mPropertyName = Objects.requireNonNull(propertyName);
         mStringValues = stringValues;
         mLongValues = longValues;
@@ -84,6 +92,7 @@
         mBooleanValues = booleanValues;
         mBytesValues = bytesValues;
         mDocumentValues = documentValues;
+        mEmbeddingValues = embeddingValues;
         checkOnlyOneArrayCanBeSet();
     }
 
@@ -129,6 +138,12 @@
         return mDocumentValues;
     }
 
+    /** Returns {@link EmbeddingVector}s in an array. */
+    @Nullable
+    public EmbeddingVector[] getEmbeddingValues() {
+        return mEmbeddingValues;
+    }
+
     /**
      * Returns the held values in an array for this property.
      *
@@ -154,6 +169,9 @@
         if (mDocumentValues != null) {
             return mDocumentValues;
         }
+        if (mEmbeddingValues != null) {
+            return mEmbeddingValues;
+        }
         return null;
     }
 
@@ -182,6 +200,9 @@
         if (mDocumentValues != null) {
             ++notNullCount;
         }
+        if (mEmbeddingValues != null) {
+            ++notNullCount;
+        }
         if (notNullCount == 0 || notNullCount > 1) {
             throw new IllegalArgumentException(
                     "One and only one type array can be set in PropertyParcel");
@@ -204,6 +225,8 @@
                 hashCode = Arrays.deepHashCode(mBytesValues);
             } else if (mDocumentValues != null) {
                 hashCode = Arrays.hashCode(mDocumentValues);
+            } else if (mEmbeddingValues != null) {
+                hashCode = Arrays.deepHashCode(mEmbeddingValues);
             }
             mHashCode = Objects.hash(mPropertyName, hashCode);
         }
@@ -227,7 +250,8 @@
                 && Arrays.equals(mDoubleValues, otherPropertyParcel.mDoubleValues)
                 && Arrays.equals(mBooleanValues, otherPropertyParcel.mBooleanValues)
                 && Arrays.deepEquals(mBytesValues, otherPropertyParcel.mBytesValues)
-                && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues);
+                && Arrays.equals(mDocumentValues, otherPropertyParcel.mDocumentValues)
+                && Arrays.deepEquals(mEmbeddingValues, otherPropertyParcel.mEmbeddingValues);
     }
 
     @Override
@@ -244,12 +268,14 @@
         private boolean[] mBooleanValues;
         private byte[][] mBytesValues;
         private GenericDocumentParcel[] mDocumentValues;
+        private EmbeddingVector[] mEmbeddingValues;
 
         public Builder(@NonNull String propertyName) {
             mPropertyName = Objects.requireNonNull(propertyName);
         }
 
         /** Sets String values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setStringValues(@NonNull String[] stringValues) {
             mStringValues = Objects.requireNonNull(stringValues);
@@ -257,6 +283,7 @@
         }
 
         /** Sets long values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setLongValues(@NonNull long[] longValues) {
             mLongValues = Objects.requireNonNull(longValues);
@@ -264,6 +291,7 @@
         }
 
         /** Sets double values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setDoubleValues(@NonNull double[] doubleValues) {
             mDoubleValues = Objects.requireNonNull(doubleValues);
@@ -271,6 +299,7 @@
         }
 
         /** Sets boolean values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setBooleanValues(@NonNull boolean[] booleanValues) {
             mBooleanValues = Objects.requireNonNull(booleanValues);
@@ -278,6 +307,7 @@
         }
 
         /** Sets a two dimension byte array. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setBytesValues(@NonNull byte[][] bytesValues) {
             mBytesValues = Objects.requireNonNull(bytesValues);
@@ -285,12 +315,21 @@
         }
 
         /** Sets document values. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setDocumentValues(@NonNull GenericDocumentParcel[] documentValues) {
             mDocumentValues = Objects.requireNonNull(documentValues);
             return this;
         }
 
+        /** Sets embedding values. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setEmbeddingValues(@NonNull EmbeddingVector[] embeddingValues) {
+            mEmbeddingValues = Objects.requireNonNull(embeddingValues);
+            return this;
+        }
+
         /** Builds a {@link PropertyParcel}. */
         @NonNull
         public PropertyParcel build() {
@@ -301,7 +340,8 @@
                     mDoubleValues,
                     mBooleanValues,
                     mBytesValues,
-                    mDocumentValues);
+                    mDocumentValues,
+                    mEmbeddingValues);
         }
     }
 }
diff --git a/framework/java/external/android/app/appsearch/stats/SchemaMigrationStats.java b/framework/java/external/android/app/appsearch/stats/SchemaMigrationStats.java
index 9efef2b..a0debbe 100644
--- a/framework/java/external/android/app/appsearch/stats/SchemaMigrationStats.java
+++ b/framework/java/external/android/app/appsearch/stats/SchemaMigrationStats.java
@@ -24,6 +24,7 @@
 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
 import android.app.appsearch.safeparcel.SafeParcelable;
 import android.os.Parcel;
+import android.os.Parcelable;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -37,9 +38,10 @@
 @SafeParcelable.Class(creator = "SchemaMigrationStatsCreator")
 public final class SchemaMigrationStats extends AbstractSafeParcelable {
     @NonNull
-    public static final SchemaMigrationStatsCreator CREATOR = new SchemaMigrationStatsCreator();
+    public static final Parcelable.Creator<SchemaMigrationStats> CREATOR =
+            new SchemaMigrationStatsCreator();
 
-    // Indicate the how a SetSchema call relative to SchemaMigration case.
+    /** Indicate the SetSchema call type relative to SchemaMigration case. */
     @IntDef(
             value = {
                 NO_MIGRATION,
@@ -51,8 +53,10 @@
 
     /** This SetSchema call is not relative to a SchemaMigration case. */
     public static final int NO_MIGRATION = 0;
+
     /** This is the first SetSchema call in Migration cases to get all incompatible changes. */
     public static final int FIRST_CALL_GET_INCOMPATIBLE = 1;
+
     /** This is the second SetSchema call in Migration cases to apply new schema changes */
     public static final int SECOND_CALL_APPLY_NEW_SCHEMA = 2;
 
diff --git a/framework/java/external/android/app/appsearch/usagereporting/ActionConstants.java b/framework/java/external/android/app/appsearch/usagereporting/ActionConstants.java
new file mode 100644
index 0000000..2943587
--- /dev/null
+++ b/framework/java/external/android/app/appsearch/usagereporting/ActionConstants.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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.usagereporting;
+
+/**
+ * Wrapper class for action constants.
+ *
+ * @hide
+ */
+public final class ActionConstants {
+    /**
+     * Unknown action type.
+     *
+     * <p>It is defined for abstract action class and compatibility, so it should not be used in any
+     * concrete instances.
+     */
+    public static final int ACTION_TYPE_UNKNOWN = 0;
+
+    /** Search action type. */
+    public static final int ACTION_TYPE_SEARCH = 1;
+
+    /** Click action type. */
+    public static final int ACTION_TYPE_CLICK = 2;
+
+    private ActionConstants() {}
+}
diff --git a/framework/java/external/android/app/appsearch/util/BundleUtil.java b/framework/java/external/android/app/appsearch/util/BundleUtil.java
index dfd8046..5df64ad 100644
--- a/framework/java/external/android/app/appsearch/util/BundleUtil.java
+++ b/framework/java/external/android/app/appsearch/util/BundleUtil.java
@@ -247,7 +247,7 @@
             // Read bundle from bytes
             parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
             parcel.setDataPosition(0);
-            return parcel.readBundle();
+            return parcel.readBundle(BundleUtil.class.getClassLoader());
         } finally {
             parcel.recycle();
         }
diff --git a/framework/java/external/android/app/appsearch/util/LogUtil.java b/framework/java/external/android/app/appsearch/util/LogUtil.java
index 7ca7865..61be70d 100644
--- a/framework/java/external/android/app/appsearch/util/LogUtil.java
+++ b/framework/java/external/android/app/appsearch/util/LogUtil.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.Size;
+import android.app.appsearch.AppSearchEnvironmentFactory;
 import android.util.Log;
 
 /**
@@ -32,6 +33,9 @@
     //  for eng builds.
     public static final boolean DEBUG = false;
 
+    public static final boolean INFO =
+            AppSearchEnvironmentFactory.getEnvironmentInstance().isInfoLoggingEnabled();
+
     /**
      * The {@link #piiTrace} logs are intended for sensitive data that can't be enabled in
      * production, so they are build-gated by this constant.
@@ -61,7 +65,7 @@
      */
     public static void piiTrace(
             @Size(min = 0, max = 23) @NonNull String tag, @NonNull String message) {
-        piiTrace(tag, message, /*fastTraceObj=*/ null, /*fullTraceObj=*/ null);
+        piiTrace(tag, message, /* fastTraceObj= */ null, /* fullTraceObj= */ null);
     }
 
     /**
@@ -76,7 +80,7 @@
             @Size(min = 0, max = 23) @NonNull String tag,
             @NonNull String message,
             @Nullable Object traceObj) {
-        piiTrace(tag, message, /*fastTraceObj=*/ traceObj, /*fullTraceObj=*/ null);
+        piiTrace(tag, message, /* fastTraceObj= */ traceObj, /* fullTraceObj= */ null);
     }
 
     /**
@@ -95,7 +99,7 @@
             @NonNull String message,
             @Nullable Object fastTraceObj,
             @Nullable Object fullTraceObj) {
-        if (PII_TRACE_LEVEL == 0) {
+        if (PII_TRACE_LEVEL == 0 || !INFO) {
             return;
         }
         StringBuilder builder = new StringBuilder("(trace) ").append(message);
diff --git a/safeparcel-processor/src/android/app/appsearch/safeparcel/SafeParcelProcessor.java b/safeparcel-processor/src/android/app/appsearch/safeparcel/SafeParcelProcessor.java
index 3ed835d..5d8f0c3 100644
--- a/safeparcel-processor/src/android/app/appsearch/safeparcel/SafeParcelProcessor.java
+++ b/safeparcel-processor/src/android/app/appsearch/safeparcel/SafeParcelProcessor.java
@@ -44,7 +44,6 @@
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.annotation.processing.SupportedSourceVersion;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
@@ -1508,6 +1507,19 @@
                                         (TypeElement) mTypes.asElement(mParcelableCreatorType),
                                         parcelableClass.asType())
                                 .toString();
+                TypeMirror parcelableType = parcelableClass.asType();
+                if (parcelableType instanceof DeclaredType) {
+                    DeclaredType declaredType = (DeclaredType) parcelableType; // Parcel<T>
+                    if (!declaredType.getTypeArguments().isEmpty()) {
+                        // If the ParcelableType is generic (ex: Parcelable.Creator<Parcel<T>>),
+                        // then expectedAlternativeCreatorTypeName needs to trim <T> part as
+                        // detectedAlternativeCreatorTypeName would only return Parcel resulting
+                        // in an incorrect ParcelCreatorType failure.
+                        String type = declaredType.getTypeArguments().get(0).toString(); // T
+                        expectedAlternativeCreatorTypeName =
+                                expectedAlternativeCreatorTypeName.replace("<" + type + ">", "");
+                    }
+                }
                 if (generatedClassName.equals(detectedCreatorTypeName)
                         || expectedAlternativeCreatorTypeName.equals(
                                 detectedAlternativeCreatorTypeName)) {
diff --git a/service/Android.bp b/service/Android.bp
index 4ef030a..730e133 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -60,6 +60,7 @@
     libs: [
         "framework-appsearch.impl",
         "framework-configinfrastructure",
+        "framework-permission-s",
         "framework-statsd.stubs.module_lib",
     ],
     optimize: {
diff --git a/service/java/com/android/server/appsearch/AppSearchComponentFactory.java b/service/java/com/android/server/appsearch/AppSearchComponentFactory.java
index e0f7a63..bd53a53 100644
--- a/service/java/com/android/server/appsearch/AppSearchComponentFactory.java
+++ b/service/java/com/android/server/appsearch/AppSearchComponentFactory.java
@@ -28,16 +28,17 @@
 
 /** This is a factory class for implementations needed based on environment for service code. */
 public final class AppSearchComponentFactory {
-    private static volatile FrameworkAppSearchConfig mConfigInstance;
+    private static volatile ServiceAppSearchConfig sConfigInstance;
 
-    public static FrameworkAppSearchConfig getConfigInstance(@NonNull Executor executor) {
-        FrameworkAppSearchConfig localRef = mConfigInstance;
+    /** Gets an instance of ServiceAppSearchConfig for the given executor. */
+    public static ServiceAppSearchConfig getConfigInstance(@NonNull Executor executor) {
+        ServiceAppSearchConfig localRef = sConfigInstance;
         if (localRef == null) {
             synchronized (AppSearchComponentFactory.class) {
-                localRef = mConfigInstance;
+                localRef = sConfigInstance;
                 if (localRef == null) {
-                    mConfigInstance = localRef = FrameworkAppSearchConfigImpl
-                            .getInstance(executor);
+                    sConfigInstance =
+                            localRef = FrameworkServiceAppSearchConfig.getInstance(executor);
                 }
             }
         }
@@ -45,15 +46,14 @@
     }
 
     @VisibleForTesting
-    static void setConfigInstanceForTest(
-            @NonNull FrameworkAppSearchConfig appSearchConfig) {
+    static void setConfigInstanceForTest(@NonNull ServiceAppSearchConfig appSearchConfig) {
         synchronized (AppSearchComponentFactory.class) {
-            mConfigInstance = appSearchConfig;
+            sConfigInstance = appSearchConfig;
         }
     }
 
     public static InternalAppSearchLogger createLoggerInstance(
-            @NonNull Context context, @NonNull FrameworkAppSearchConfig config) {
+            @NonNull Context context, @NonNull ServiceAppSearchConfig config) {
         return new PlatformLogger(context, config);
     }
 
@@ -61,6 +61,5 @@
         return new VisibilityCheckerImpl(context);
     }
 
-    private AppSearchComponentFactory() {
-    }
+    private AppSearchComponentFactory() {}
 }
diff --git a/service/java/com/android/server/appsearch/AppSearchMaintenanceService.java b/service/java/com/android/server/appsearch/AppSearchMaintenanceService.java
new file mode 100644
index 0000000..51f41ee
--- /dev/null
+++ b/service/java/com/android/server/appsearch/AppSearchMaintenanceService.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.util.LogUtil;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.PersistableBundle;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalManagerRegistry;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AppSearchMaintenanceService extends JobService {
+    private static final String TAG = "AppSearchMaintenanceSer";
+
+    private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
+    private static final String EXTRA_USER_ID = "user_id";
+
+    /**
+     * Generate job ids in the range (MIN_APPSEARCH_MAINTENANCE_JOB_ID,
+     * MIN_APPSEARCH_MAINTENANCE_JOB_ID + MAX_USER_ID) to avoid conflicts with other jobs scheduled
+     * by the system service. The range corresponds to 21475 job ids, which is the maximum number of
+     * user ids in the system.
+     *
+     * @see com.android.server.pm.UserManagerService#MAX_USER_ID
+     */
+    public static final int MIN_APPSEARCH_MAINTENANCE_JOB_ID = 461234957; // 0x1B7DE30D
+
+    /**
+     * A mapping of userId-to-CancellationSignal. Since we schedule a separate job for each user,
+     * this JobService might be executing simultaneously for the various users, so we need to keep
+     * track of the cancellation signal for each user update so we stop the appropriate update when
+     * necessary.
+     */
+    @GuardedBy("mSignalsLocked")
+    private final SparseArray<CancellationSignal> mSignalsLocked = new SparseArray<>();
+
+    /**
+     * Schedule the daily fully persist job for the given user.
+     *
+     * <p>The job will persists all pending mutation operation to disk.
+     */
+    static void scheduleFullyPersistJob(
+            @NonNull Context context, @UserIdInt int userId, long intervalMillis) {
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+
+        final PersistableBundle extras = new PersistableBundle();
+        extras.putInt(EXTRA_USER_ID, userId);
+        JobInfo jobInfo =
+                new JobInfo.Builder(
+                                MIN_APPSEARCH_MAINTENANCE_JOB_ID
+                                        + userId, // must be unique across uid
+                                new ComponentName(context, AppSearchMaintenanceService.class))
+                        .setPeriodic(intervalMillis) // run once a day, at most
+                        .setExtras(extras)
+                        .setPersisted(true) // persist across reboots
+                        .setRequiresBatteryNotLow(true)
+                        .setRequiresCharging(true)
+                        .setRequiresDeviceIdle(true)
+                        .build();
+        jobScheduler.schedule(jobInfo);
+        if (LogUtil.DEBUG) {
+            Log.v(TAG, "Scheduling the daily AppSearch full persist job");
+        }
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        try {
+            int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ -1);
+            if (userId == -1) {
+                return false;
+            }
+
+            final CancellationSignal signal;
+            synchronized (mSignalsLocked) {
+                CancellationSignal oldSignal = mSignalsLocked.get(userId);
+                if (oldSignal != null) {
+                    // This could happen if we attempt to schedule a new job for the user while
+                    // there's
+                    // one already running.
+                    Log.w(TAG, "Old maintenance job still running for user " + userId);
+                    oldSignal.cancel();
+                }
+                signal = new CancellationSignal();
+                mSignalsLocked.put(userId, signal);
+            }
+            EXECUTOR.execute(() -> doFullyPersistJobForUser(this, params, userId, signal));
+            return true;
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "AppSearchMaintenanceService.onStartJob() failed ", e);
+            return false;
+        }
+    }
+
+    /** Triggers full persist job for the given user directly. */
+    @VisibleForTesting
+    @CanIgnoreReturnValue
+    protected boolean doFullyPersistJobForUser(
+            Context context, JobParameters params, int userId, CancellationSignal signal) {
+        try {
+            AppSearchManagerService.LocalService service =
+                    LocalManagerRegistry.getManager(AppSearchManagerService.LocalService.class);
+            if (service == null) {
+                Log.e(
+                        TAG,
+                        "Background job failed to trigger Full persist because "
+                                + "AppSearchManagerService.LocalService is not available.");
+                // Cancel unnecessary background full persist job if AppSearch local service is not
+                // registered
+                cancelFullyPersistJobIfScheduled(context, userId);
+                return false;
+            }
+            service.doFullyPersistForUser(userId);
+        } catch (Throwable t) {
+            Log.e(TAG, "Run Daily optimize job failed.", t);
+            jobFinished(params, /* wantsReschedule= */ true);
+            return false;
+        } finally {
+            jobFinished(params, /* wantsReschedule= */ false);
+            synchronized (mSignalsLocked) {
+                if (signal == mSignalsLocked.get(userId)) {
+                    mSignalsLocked.remove(userId);
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        try {
+            final int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue */ -1);
+            if (userId == -1) {
+                return false;
+            }
+            if (LogUtil.DEBUG) {
+                Log.d(
+                        TAG,
+                        "AppSearch maintenance job is stopped; id="
+                                + params.getJobId()
+                                + ", reason="
+                                + params.getStopReason());
+            }
+            synchronized (mSignalsLocked) {
+                final CancellationSignal signal = mSignalsLocked.get(userId);
+                if (signal != null) {
+                    signal.cancel();
+                    mSignalsLocked.remove(userId);
+                    // We had to stop the job early. Request reschedule.
+                    return true;
+                }
+            }
+            Log.e(TAG, "JobScheduler stopped an update that wasn't happening...");
+            return false;
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "AppSearchMaintenanceService.onStopJob() failed ", e);
+        }
+        return false;
+    }
+
+    /**
+     * Cancel full persist job for the given user.
+     *
+     * @param userId The user id for whom the full persist job needs to be cancelled.
+     */
+    public static void cancelFullyPersistJobIfScheduled(
+            @NonNull Context context, @UserIdInt int userId) {
+        Objects.requireNonNull(context);
+        int jobId = MIN_APPSEARCH_MAINTENANCE_JOB_ID + userId;
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        if (jobScheduler.getPendingJob(jobId) != null) {
+            jobScheduler.cancel(jobId);
+            if (LogUtil.DEBUG) {
+                Log.v(TAG, "Canceled job " + jobId + " for user " + userId);
+            }
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/AppSearchManagerService.java b/service/java/com/android/server/appsearch/AppSearchManagerService.java
index 18a20a0..5d30a11 100644
--- a/service/java/com/android/server/appsearch/AppSearchManagerService.java
+++ b/service/java/com/android/server/appsearch/AppSearchManagerService.java
@@ -16,9 +16,15 @@
 package com.android.server.appsearch;
 
 import static android.app.appsearch.AppSearchResult.RESULT_DENIED;
+import static android.app.appsearch.AppSearchResult.RESULT_INTERNAL_ERROR;
+import static android.app.appsearch.AppSearchResult.RESULT_INVALID_ARGUMENT;
+import static android.app.appsearch.AppSearchResult.RESULT_NOT_FOUND;
 import static android.app.appsearch.AppSearchResult.RESULT_OK;
 import static android.app.appsearch.AppSearchResult.RESULT_RATE_LIMITED;
+import static android.app.appsearch.AppSearchResult.RESULT_SECURITY_ERROR;
+import static android.app.appsearch.AppSearchResult.RESULT_TIMED_OUT;
 import static android.app.appsearch.AppSearchResult.throwableToFailedResult;
+import static android.app.appsearch.functions.AppFunctionManager.PERMISSION_BIND_APP_FUNCTION_SERVICE;
 import static android.os.Process.INVALID_UID;
 
 import static com.android.server.appsearch.external.localstorage.stats.SearchStats.VISIBILITY_SCOPE_GLOBAL;
@@ -27,9 +33,9 @@
 import static com.android.server.appsearch.util.ServiceImplHelper.invokeCallbackOnResult;
 
 import android.annotation.BinderThread;
-import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.annotation.WorkerThread;
 import android.app.appsearch.AppSearchBatchResult;
 import android.app.appsearch.AppSearchEnvironment;
@@ -45,13 +51,16 @@
 import android.app.appsearch.SetSchemaResponse;
 import android.app.appsearch.SetSchemaResponse.MigrationFailure;
 import android.app.appsearch.StorageInfo;
-import android.app.appsearch.aidl.AppSearchAttributionSource;
+import android.app.appsearch.aidl.AppSearchBatchResultParcel;
 import android.app.appsearch.aidl.AppSearchResultParcel;
+import android.app.appsearch.aidl.ExecuteAppFunctionAidlRequest;
+import android.app.appsearch.aidl.GetDocumentsAidlRequest;
 import android.app.appsearch.aidl.GetNamespacesAidlRequest;
 import android.app.appsearch.aidl.GetNextPageAidlRequest;
 import android.app.appsearch.aidl.GetSchemaAidlRequest;
 import android.app.appsearch.aidl.GetStorageInfoAidlRequest;
 import android.app.appsearch.aidl.GlobalSearchAidlRequest;
+import android.app.appsearch.aidl.IAppFunctionService;
 import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
 import android.app.appsearch.aidl.IAppSearchManager;
 import android.app.appsearch.aidl.IAppSearchObserverProxy;
@@ -64,36 +73,51 @@
 import android.app.appsearch.aidl.RegisterObserverCallbackAidlRequest;
 import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
 import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
+import android.app.appsearch.aidl.ReportUsageAidlRequest;
 import android.app.appsearch.aidl.SearchAidlRequest;
 import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
 import android.app.appsearch.aidl.SetSchemaAidlRequest;
 import android.app.appsearch.aidl.UnregisterObserverCallbackAidlRequest;
 import android.app.appsearch.aidl.WriteSearchResultsToFileAidlRequest;
 import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.functions.AppFunctionService;
+import android.app.appsearch.functions.ExecuteAppFunctionRequest;
+import android.app.appsearch.functions.SafeOneTimeAppSearchResultCallback;
+import android.app.appsearch.functions.ServiceCallHelper;
+import android.app.appsearch.functions.ServiceCallHelper.ServiceUsageCompleteListener;
+import android.app.appsearch.functions.ServiceCallHelperImpl;
 import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.stats.SchemaMigrationStats;
+import android.app.appsearch.util.ExceptionUtil;
 import android.app.appsearch.util.LogUtil;
+import android.app.role.RoleManager;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageStats;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.SystemService;
 import com.android.server.appsearch.external.localstorage.stats.CallStats;
 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
 import com.android.server.appsearch.external.localstorage.stats.SearchStats;
 import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
+import com.android.server.appsearch.external.localstorage.usagereporting.SearchSessionStatsExtractor;
 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
 import com.android.server.appsearch.observer.AppSearchObserverProxy;
 import com.android.server.appsearch.stats.StatsCollector;
@@ -101,8 +125,8 @@
 import com.android.server.appsearch.transformer.EnterpriseSearchSpecTransformer;
 import com.android.server.appsearch.util.AdbDumpUtil;
 import com.android.server.appsearch.util.ApiCallRecord;
-import com.android.server.appsearch.util.ExceptionUtil;
 import com.android.server.appsearch.util.ExecutorManager;
+import com.android.server.appsearch.util.PackageManagerUtil;
 import com.android.server.appsearch.util.ServiceImplHelper;
 import com.android.server.appsearch.visibilitystore.FrameworkCallerAccess;
 import com.android.server.usage.StorageStatsManagerLocal;
@@ -134,6 +158,8 @@
  */
 public class AppSearchManagerService extends SystemService {
     private static final String TAG = "AppSearchManagerService";
+    @VisibleForTesting
+    static final String SYSTEM_UI_INTELLIGENCE = "android.app.role.SYSTEM_UI_INTELLIGENCE";
 
     /**
      * An executor for system activity not tied to any particular user.
@@ -147,34 +173,50 @@
     private final Context mContext;
     private final ExecutorManager mExecutorManager;
     private final AppSearchEnvironment mAppSearchEnvironment;
-    private final FrameworkAppSearchConfig mAppSearchConfig;
+    private final ServiceAppSearchConfig mAppSearchConfig;
 
     private PackageManager mPackageManager;
+    private RoleManager mRoleManager;
     private ServiceImplHelper mServiceImplHelper;
     private AppSearchUserInstanceManager mAppSearchUserInstanceManager;
 
     // Keep a reference for the lifecycle instance, so we can access other services like
     // ContactsIndexer for dumpsys purpose.
     private final AppSearchModule.Lifecycle mLifecycle;
+    private final ServiceCallHelper<IAppFunctionService> mAppFunctionServiceCallHelper;
+    private final SearchSessionStatsExtractor mSearchSessionStatsExtractor;
 
     public AppSearchManagerService(Context context, AppSearchModule.Lifecycle lifecycle) {
+        this(context, lifecycle, new ServiceCallHelperImpl<>(
+                context, IAppFunctionService.Stub::asInterface, SHARED_EXECUTOR));
+    }
+
+    @VisibleForTesting
+    public AppSearchManagerService(
+            Context context,
+            AppSearchModule.Lifecycle lifecycle,
+            ServiceCallHelper<IAppFunctionService> appFunctionServiceCallHelper) {
         super(context);
-        mContext = context;
-        mLifecycle = lifecycle;
+        mContext = Objects.requireNonNull(context);
+        mLifecycle = Objects.requireNonNull(lifecycle);
         mAppSearchEnvironment = AppSearchEnvironmentFactory.getEnvironmentInstance();
         mAppSearchConfig = AppSearchComponentFactory.getConfigInstance(SHARED_EXECUTOR);
         mExecutorManager = new ExecutorManager(mAppSearchConfig);
+        mAppFunctionServiceCallHelper = Objects.requireNonNull(appFunctionServiceCallHelper);
+        mSearchSessionStatsExtractor = new SearchSessionStatsExtractor();
     }
 
     @Override
     public void onStart() {
         publishBinderService(Context.APP_SEARCH_SERVICE, new Stub());
         mPackageManager = getContext().getPackageManager();
+        mRoleManager = getContext().getSystemService(RoleManager.class);
         mServiceImplHelper = new ServiceImplHelper(mContext, mExecutorManager);
         mAppSearchUserInstanceManager = AppSearchUserInstanceManager.getInstance();
         registerReceivers();
         LocalManagerRegistry.getManager(StorageStatsManagerLocal.class)
                 .registerStorageStatsAugmenter(new AppSearchStorageStatsAugmenter(), TAG);
+        LocalManagerRegistry.addManager(LocalService.class, new LocalService());
     }
 
     @Override
@@ -306,10 +348,13 @@
         Objects.requireNonNull(user);
         UserHandle userHandle = user.getUserHandle();
         mServiceImplHelper.setUserIsLocked(userHandle, false);
-        mExecutorManager.getOrCreateUserExecutor(userHandle).execute(() -> {
-            try {
-                // Only clear the package's data if AppSearch exists for this user.
-                if (mAppSearchEnvironment.getAppSearchDir(mContext, userHandle).exists()) {
+
+        // Only schedule task if AppSearch exists for this user.
+        if (mAppSearchEnvironment.getAppSearchDir(mContext, userHandle).exists()) {
+            mExecutorManager.getOrCreateUserExecutor(userHandle).execute(() -> {
+                // Try to prune garbage package data, this is to recover if user remove a package
+                // and reboot the device before we prune the package data.
+                try {
                     Context userContext = mAppSearchEnvironment
                             .createContextAsUser(mContext, userHandle);
                     AppSearchUserInstance instance =
@@ -326,12 +371,22 @@
                     }
                     packagesToKeep.add(VisibilityStore.VISIBILITY_PACKAGE_NAME);
                     instance.getAppSearchImpl().prunePackageData(packagesToKeep);
+                } catch (AppSearchException | RuntimeException e) {
+                    Log.e(TAG, "Unable to prune packages for " + user, e);
+                    ExceptionUtil.handleException(e);
                 }
-            } catch (AppSearchException | RuntimeException e) {
-                Log.e(TAG, "Unable to prune packages for " + user, e);
-                ExceptionUtil.handleException(e);
-            }
-        });
+
+                // Try to schedule fully persist job.
+                try {
+                    AppSearchMaintenanceService.scheduleFullyPersistJob(mContext,
+                            userHandle.getIdentifier(),
+                            mAppSearchConfig.getCachedFullyPersistJobIntervalMillis());
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Unable to schedule fully persist job for " + user, e);
+                    ExceptionUtil.handleException(e);
+                }
+            });
+        }
     }
 
     @Override
@@ -342,18 +397,34 @@
 
     private void onUserStopping(@NonNull UserHandle userHandle) {
         Objects.requireNonNull(userHandle);
-        Log.i(TAG, "Shutting down AppSearch for user " + userHandle);
+        if (LogUtil.INFO) {
+            Log.i(TAG, "Shutting down AppSearch for user " + userHandle);
+        }
         try {
             mServiceImplHelper.setUserIsLocked(userHandle, true);
             mExecutorManager.shutDownAndRemoveUserExecutor(userHandle);
             mAppSearchUserInstanceManager.closeAndRemoveUserInstance(userHandle);
-            Log.i(TAG, "Removed AppSearchImpl instance for: " + userHandle);
+            AppSearchMaintenanceService.cancelFullyPersistJobIfScheduled(
+                    mContext, userHandle.getIdentifier());
+            if (LogUtil.INFO) {
+                Log.i(TAG, "Removed AppSearchImpl instance for: " + userHandle);
+            }
         } catch (InterruptedException | RuntimeException e) {
             Log.e(TAG, "Unable to remove data for: " + userHandle, e);
             ExceptionUtil.handleException(e);
         }
     }
 
+    class LocalService {
+        /** Persist all pending mutation operation to disk for the given user. */
+        public void doFullyPersistForUser(@UserIdInt int userId) throws AppSearchException {
+            UserHandle targetUser = UserHandle.getUserHandleForUid(userId);
+            AppSearchUserInstance instance =
+                mAppSearchUserInstanceManager.getUserInstance(targetUser);
+            instance.getAppSearchImpl().persistToDisk(PersistType.Code.FULL);
+        }
+    }
+
     private class Stub extends IAppSearchManager.Stub {
         @Override
         public void setSchema(
@@ -366,8 +437,7 @@
             long verifyIncomingCallLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -403,9 +473,8 @@
                                     request.getSchemaVersion(),
                                     setSchemaStatsBuilder);
                     ++operationSuccessCount;
-                    invokeCallbackOnResult(
-                            callback,
-                            AppSearchResult.newSuccessfulResult(internalSetSchemaResponse));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel
+                            .fromInternalSetSchemaResponse(internalSetSchemaResponse));
 
                     // Schedule a task to dispatch change notifications. See requirements for where
                     // the method is called documented in the method description.
@@ -440,7 +509,8 @@
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
@@ -487,8 +557,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -497,7 +566,7 @@
                     targetUser);
             if (userToQuery == null) {
                 // Return an empty response if we tried to and couldn't get the enterprise user
-                invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(
+                invokeCallbackOnResult(callback, AppSearchResultParcel.fromGetSchemaResponse(
                         new GetSchemaResponse.Builder().build()));
                 return;
             }
@@ -532,13 +601,15 @@
                                     new FrameworkCallerAccess(request.getCallerAttributionSource(),
                                             callerHasSystemAccess, request.isForEnterprise()));
                     ++operationSuccessCount;
-                    invokeCallbackOnResult(
-                            callback, AppSearchResult.newSuccessfulResult(response));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel
+                            .fromGetSchemaResponse(response)
+                    );
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
@@ -579,8 +650,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -602,13 +672,15 @@
                             instance.getAppSearchImpl().getNamespaces(
                                     callingPackageName, request.getDatabaseName());
                     ++operationSuccessCount;
-                    invokeCallbackOnResult(
-                            callback, AppSearchResult.newSuccessfulResult(namespaces));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel
+                            .fromStringList(namespaces)
+                    );
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
@@ -650,8 +722,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -667,6 +738,8 @@
                 AppSearchUserInstance instance = null;
                 int operationSuccessCount = 0;
                 int operationFailureCount = 0;
+                List<GenericDocument> takenActionGenericDocuments = null;  // initialize later
+
                 try {
                     AppSearchBatchResult.Builder<String, Void> resultBuilder =
                             new AppSearchBatchResult.Builder<>();
@@ -676,7 +749,7 @@
                     List<GenericDocumentParcel> takenActionDocumentParcels =
                             request.getDocumentsParcel().getTakenActionGenericDocumentParcels();
 
-                    // Write GenericDocuments
+                    // Write GenericDocument of general documents
                     for (int i = 0; i < documentParcels.size(); i++) {
                         GenericDocument document = new GenericDocument(documentParcels.get(i));
                         try {
@@ -700,10 +773,15 @@
                         }
                     }
 
-                    // Write TakenActions
+                    // Write GenericDocument of taken actions
+                    if (!takenActionDocumentParcels.isEmpty()) {
+                        takenActionGenericDocuments =
+                                new ArrayList<>(takenActionDocumentParcels.size());
+                    }
                     for (int i = 0; i < takenActionDocumentParcels.size(); i++) {
                         GenericDocument document =
                                 new GenericDocument(takenActionDocumentParcels.get(i));
+                        takenActionGenericDocuments.add(document);
                         try {
                             instance.getAppSearchImpl().putDocument(
                                     callingPackageName,
@@ -727,7 +805,8 @@
 
                     // Now that the batch has been written. Persist the newly written data.
                     instance.getAppSearchImpl().persistToDisk(PersistType.Code.LITE);
-                    invokeCallbackOnResult(callback, resultBuilder.build());
+                    invokeCallbackOnResult(callback, AppSearchBatchResultParcel
+                            .fromStringToVoid(resultBuilder.build()));
 
                     // Schedule a task to dispatch change notifications. See requirements for where
                     // the method is called documented in the method description.
@@ -766,6 +845,16 @@
                                 .setNumOperationsSucceeded(operationSuccessCount)
                                 .setNumOperationsFailed(operationFailureCount)
                                 .build());
+
+                        // Extract metrics from taken action generic documents and add log.
+                        if (takenActionGenericDocuments != null
+                                && !takenActionGenericDocuments.isEmpty()) {
+                            instance.getLogger()
+                                    .logStats(mSearchSessionStatsExtractor.extract(
+                                            callingPackageName,
+                                            request.getDatabaseName(),
+                                            takenActionGenericDocuments));
+                        }
                     }
                 }
             });
@@ -781,54 +870,42 @@
 
         @Override
         public void getDocuments(
-                @NonNull AppSearchAttributionSource callerAttributionSource,
-                @NonNull String targetPackageName,
-                @NonNull String databaseName,
-                @NonNull String namespace,
-                @NonNull List<String> ids,
-                @NonNull Map<String, List<String>> typePropertyPaths,
-                @NonNull UserHandle userHandle,
-                @ElapsedRealtimeLong long binderCallStartTimeMillis,
-                boolean isForEnterprise,
+                @NonNull GetDocumentsAidlRequest request,
                 @NonNull IAppSearchBatchResultCallback callback) {
-            Objects.requireNonNull(callerAttributionSource);
-            Objects.requireNonNull(targetPackageName);
-            Objects.requireNonNull(databaseName);
-            Objects.requireNonNull(namespace);
-            Objects.requireNonNull(ids);
-            Objects.requireNonNull(typePropertyPaths);
-            Objects.requireNonNull(userHandle);
+            Objects.requireNonNull(request);
             Objects.requireNonNull(callback);
 
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
-                    callerAttributionSource, userHandle, callback);
-            String callingPackageName =
-                    Objects.requireNonNull(callerAttributionSource.getPackageName());
+                    request.getCallerAttributionSource(), request.getUserHandle(), callback);
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
             // Get the enterprise user for enterprise calls
-            UserHandle userToQuery = mServiceImplHelper.getUserToQuery(isForEnterprise, targetUser);
+            UserHandle userToQuery = mServiceImplHelper.getUserToQuery(
+                    request.isForEnterprise(), targetUser);
             if (userToQuery == null) {
                 // Return an empty batch result if we tried to and couldn't get the enterprise user
-                invokeCallbackOnResult(callback,
-                        new AppSearchBatchResult.Builder<String, GenericDocumentParcel>().build());
+                invokeCallbackOnResult(callback, AppSearchBatchResultParcel
+                        .fromStringToGenericDocumentParcel(new AppSearchBatchResult
+                                .Builder<String, GenericDocumentParcel>().build()));
                 return;
             }
             // TODO(b/319315074): consider removing local getDocument and just use globalGetDocument
             //  instead; this would simplify the code and assure us that enterprise calls definitely
             //  go through visibility checks
-            boolean global = isGlobalCall(callingPackageName, targetPackageName, isForEnterprise);
+            boolean global = isGlobalCall(callingPackageName, request.getTargetPackageName(),
+                    request.isForEnterprise());
             // We deny based on the calling package and calling database names. If the calling
             // package does not match the target package, then the call is global and the target
             // database is not a calling database.
-            String callingDatabaseName = global ? null : databaseName;
+            String callingDatabaseName = global ? null : request.getDatabaseName();
             int callType = global ? CallStats.CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID
                     : CallStats.CALL_TYPE_GET_DOCUMENTS;
             if (checkCallDenied(callingPackageName, callingDatabaseName, callType, callback,
-                    targetUser, binderCallStartTimeMillis, totalLatencyStartTimeMillis,
-                    /* numOperations= */ ids.size())) {
+                    targetUser, request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
+                    /* numOperations= */ request.getGetByDocumentIdRequest().getIds().size())) {
                 return;
             }
             boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
@@ -841,38 +918,44 @@
                     AppSearchBatchResult.Builder<String, GenericDocumentParcel> resultBuilder =
                             new AppSearchBatchResult.Builder<>();
                     instance = mAppSearchUserInstanceManager.getUserInstance(userToQuery);
-                    for (int i = 0; i < ids.size(); i++) {
-                        String id = ids.get(i);
+                    for (String id : request.getGetByDocumentIdRequest().getIds()) {
                         try {
                             GenericDocument document;
                             if (global) {
                                 boolean callerHasSystemAccess = instance.getVisibilityChecker()
-                                        .doesCallerHaveSystemAccess(callerAttributionSource
-                                                .getPackageName());
-                                if (isForEnterprise) {
+                                        .doesCallerHaveSystemAccess(
+                                                request.getCallerAttributionSource()
+                                                        .getPackageName());
+                                Map<String, List<String>> typePropertyPaths =
+                                        request.getGetByDocumentIdRequest().getProjections();
+                                if (request.isForEnterprise()) {
                                     EnterpriseSearchSpecTransformer.transformPropertiesMap(
                                             typePropertyPaths);
                                 }
                                 document = instance.getAppSearchImpl().globalGetDocument(
-                                        targetPackageName,
-                                        databaseName,
-                                        namespace,
+                                        request.getTargetPackageName(),
+                                        request.getDatabaseName(),
+                                        request.getGetByDocumentIdRequest().getNamespace(),
                                         id,
                                         typePropertyPaths,
-                                        new FrameworkCallerAccess(callerAttributionSource,
-                                                callerHasSystemAccess, isForEnterprise));
-                                if (isForEnterprise) {
+                                        new FrameworkCallerAccess(
+                                                request.getCallerAttributionSource(),
+                                                callerHasSystemAccess,
+                                                request.isForEnterprise()));
+                                if (request.isForEnterprise()) {
                                     document =
                                             EnterpriseSearchResultPageTransformer.transformDocument(
-                                                    targetPackageName, databaseName, document);
+                                                    request.getTargetPackageName(),
+                                                    request.getDatabaseName(),
+                                                    document);
                                 }
                             } else {
                                 document = instance.getAppSearchImpl().getDocument(
-                                        targetPackageName,
-                                        databaseName,
-                                        namespace,
+                                        request.getTargetPackageName(),
+                                        request.getDatabaseName(),
+                                        request.getGetByDocumentIdRequest().getNamespace(),
                                         id,
-                                        typePropertyPaths);
+                                        request.getGetByDocumentIdRequest().getProjections());
                             }
                             ++operationSuccessCount;
                             resultBuilder.setSuccess(id, document.getDocumentParcel());
@@ -888,7 +971,8 @@
                             ++operationFailureCount;
                         }
                     }
-                    invokeCallbackOnResult(callback, resultBuilder.build());
+                    invokeCallbackOnResult(callback, AppSearchBatchResultParcel
+                            .fromStringToGenericDocumentParcel(resultBuilder.build()));
                 } catch (RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
@@ -898,12 +982,13 @@
                     // TODO(b/261959320) add outstanding latency fields in AppSearch stats
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
-                                2 * (int) (totalLatencyStartTimeMillis - binderCallStartTimeMillis);
+                                2 * (int) (totalLatencyStartTimeMillis -
+                                        request.getBinderCallStartTimeMillis());
                         int totalLatencyMillis =
                                 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
                         instance.getLogger().logStats(new CallStats.Builder()
                                 .setPackageName(callingPackageName)
-                                .setDatabase(databaseName)
+                                .setDatabase(request.getDatabaseName())
                                 .setStatusCode(statusCode)
                                 .setTotalLatencyMillis(totalLatencyMillis)
                                 .setCallType(callType)
@@ -919,8 +1004,9 @@
             });
             if (!callAccepted) {
                 logRateLimitedOrCallDeniedCallStats(callingPackageName, callingDatabaseName,
-                        callType, targetUser, binderCallStartTimeMillis,
-                        totalLatencyStartTimeMillis, /* numOperations= */ ids.size(),
+                        callType, targetUser, request.getBinderCallStartTimeMillis(),
+                        totalLatencyStartTimeMillis,
+                        /* numOperations= */ request.getGetByDocumentIdRequest().getIds().size(),
                         RESULT_RATE_LIMITED);
 
             }
@@ -936,8 +1022,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -964,12 +1049,13 @@
                     ++operationSuccessCount;
                     invokeCallbackOnResult(
                             callback,
-                            AppSearchResult.newSuccessfulResult(searchResultPage));
+                            AppSearchResultParcel.fromSearchResultPage(searchResultPage));
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
@@ -1010,8 +1096,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1021,7 +1106,7 @@
             if (userToQuery == null) {
                 // Return an empty result if we tried to and couldn't get the enterprise user
                 invokeCallbackOnResult(callback,
-                        AppSearchResult.newSuccessfulResult(new SearchResultPage()));
+                        AppSearchResultParcel.fromSearchResultPage(new SearchResultPage()));
                 return;
             }
             if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
@@ -1057,12 +1142,13 @@
                     ++operationSuccessCount;
                     invokeCallbackOnResult(
                             callback,
-                            AppSearchResult.newSuccessfulResult(searchResultPage));
+                            AppSearchResultParcel.fromSearchResultPage(searchResultPage));
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
@@ -1102,8 +1188,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1113,7 +1198,7 @@
             if (userToQuery == null) {
                 // Return an empty result if we tried to and couldn't get the enterprise user
                 invokeCallbackOnResult(callback,
-                        AppSearchResult.newSuccessfulResult(new SearchResultPage()));
+                        AppSearchResultParcel.fromSearchResultPage(new SearchResultPage()));
                 return;
             }
             // Enterprise session calls are considered global for CallStats logging
@@ -1155,12 +1240,13 @@
                     ++operationSuccessCount;
                     invokeCallbackOnResult(
                             callback,
-                            AppSearchResult.newSuccessfulResult(searchResultPage));
+                            AppSearchResultParcel.fromSearchResultPage(searchResultPage));
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
@@ -1206,8 +1292,7 @@
                     // Return if we tried to and couldn't get the enterprise user
                     return;
                 }
-                String callingPackageName = Objects.requireNonNull(
-                        request.getCallerAttributionSource().getPackageName());
+                String callingPackageName = request.getCallerAttributionSource().getPackageName();
                 if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
                         CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, targetUser,
                         request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
@@ -1278,8 +1363,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1321,12 +1405,13 @@
                                     /* sStatsBuilder= */ null);
                         }
                     }
-                    invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
                 } catch (AppSearchException | IOException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis = 2 * (int) (totalLatencyStartTimeMillis
@@ -1367,8 +1452,7 @@
             long callStatsTotalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1433,13 +1517,14 @@
                     schemaMigrationStatsBuilder
                             .setTotalSuccessMigratedDocumentCount(operationSuccessCount)
                             .setMigrationFailureCount(migrationFailures.size());
-                    invokeCallbackOnResult(callback,
-                            AppSearchResult.newSuccessfulResult(migrationFailures));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel
+                            .fromMigrationFailuresList(migrationFailures));
                 } catch (AppSearchException | IOException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         long latencyEndTimeMillis =
@@ -1498,8 +1583,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1527,13 +1611,14 @@
                                     request.getSearchSuggestionSpec());
                     ++operationSuccessCount;
                     invokeCallbackOnResult(
-                            callback,
-                            AppSearchResult.newSuccessfulResult(searchSuggestionResults));
+                            callback, AppSearchResultParcel
+                                    .fromSearchSuggestionResultList(searchSuggestionResults));
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
@@ -1567,40 +1652,27 @@
 
         @Override
         public void reportUsage(
-                @NonNull AppSearchAttributionSource callerAttributionSource,
-                @NonNull String targetPackageName,
-                @NonNull String databaseName,
-                @NonNull String namespace,
-                @NonNull String documentId,
-                long usageTimeMillis,
-                boolean systemUsage,
-                @NonNull UserHandle userHandle,
-                @ElapsedRealtimeLong long binderCallStartTimeMillis,
+                @NonNull ReportUsageAidlRequest request,
                 @NonNull IAppSearchResultCallback callback) {
-            Objects.requireNonNull(callerAttributionSource);
-            Objects.requireNonNull(targetPackageName);
-            Objects.requireNonNull(databaseName);
-            Objects.requireNonNull(namespace);
-            Objects.requireNonNull(documentId);
-            Objects.requireNonNull(userHandle);
+            Objects.requireNonNull(request);
             Objects.requireNonNull(callback);
 
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
-                    callerAttributionSource, userHandle, callback);
-            String callingPackageName =
-                    Objects.requireNonNull(callerAttributionSource.getPackageName());
+                    request.getCallerAttributionSource(), request.getUserHandle(), callback);
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
             // We deny based on the calling package and calling database names. If the API call is
             // intended for system usage, then the call is global, and the target database is not a
             // calling database.
-            String callingDatabaseName = systemUsage ? null : databaseName;
-            int callType = systemUsage ? CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE
+            String callingDatabaseName = request.isSystemUsage()
+                    ? null : request.getDatabaseName();
+            int callType = request.isSystemUsage() ? CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE
                     : CallStats.CALL_TYPE_REPORT_USAGE;
             if (checkCallDenied(callingPackageName, callingDatabaseName, callType, callback,
-                    targetUser, binderCallStartTimeMillis, totalLatencyStartTimeMillis,
+                    targetUser, request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
                     /* numOperations= */ 1)) {
                 return;
             }
@@ -1613,41 +1685,46 @@
                 int operationFailureCount = 0;
                 try {
                     instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
-                    if (systemUsage) {
+                    if (request.isSystemUsage()) {
                         if (!instance.getVisibilityChecker().doesCallerHaveSystemAccess(
                                 callingPackageName)) {
-                            throw new AppSearchException(AppSearchResult.RESULT_SECURITY_ERROR,
+                            throw new AppSearchException(RESULT_SECURITY_ERROR,
                                     callingPackageName
                                             + " does not have access to report system usage");
                         }
                     } else {
-                        if (!callingPackageName.equals(targetPackageName)) {
-                            throw new AppSearchException(AppSearchResult.RESULT_SECURITY_ERROR,
+                        if (!callingPackageName.equals(request.getTargetPackageName())) {
+                            throw new AppSearchException(RESULT_SECURITY_ERROR,
                                     "Cannot report usage to different package: "
-                                            + targetPackageName + " from package: "
+                                            + request.getTargetPackageName() + " from package: "
                                             + callingPackageName);
                         }
                     }
 
-                    instance.getAppSearchImpl().reportUsage(targetPackageName, databaseName,
-                            namespace, documentId, usageTimeMillis, systemUsage);
+                    instance.getAppSearchImpl().reportUsage(request.getTargetPackageName(),
+                            request.getDatabaseName(),
+                            request.getReportUsageRequest().getNamespace(),
+                            request.getReportUsageRequest().getDocumentId(),
+                            request.getReportUsageRequest().getUsageTimestampMillis(),
+                            request.isSystemUsage());
                     ++operationSuccessCount;
-                    invokeCallbackOnResult(
-                            callback, AppSearchResult.newSuccessfulResult(/* value= */ null));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
-                                2 * (int) (totalLatencyStartTimeMillis - binderCallStartTimeMillis);
+                                2 * (int) (totalLatencyStartTimeMillis -
+                                        request.getBinderCallStartTimeMillis());
                         int totalLatencyMillis =
                                 (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
                         instance.getLogger().logStats(new CallStats.Builder()
                                 .setPackageName(callingPackageName)
-                                .setDatabase(databaseName)
+                                .setDatabase(request.getDatabaseName())
                                 .setStatusCode(statusCode)
                                 .setTotalLatencyMillis(totalLatencyMillis)
                                 .setCallType(callType)
@@ -1663,7 +1740,7 @@
             });
             if (!callAccepted) {
                 logRateLimitedOrCallDeniedCallStats(callingPackageName, callingDatabaseName,
-                        callType, targetUser, binderCallStartTimeMillis,
+                        callType, targetUser, request.getBinderCallStartTimeMillis(),
                         totalLatencyStartTimeMillis,
                         /* numOperations= */ 1, RESULT_RATE_LIMITED);
             }
@@ -1679,15 +1756,14 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
             if (checkCallDenied(callingPackageName, request.getDatabaseName(),
                     CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, callback, targetUser,
                     request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
-                    /* numOperations= */ request.getIds().size())) {
+                    /* numOperations= */ request.getRemoveByDocumentIdRequest().getIds().size())) {
                 return;
             }
             boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(targetUser,
@@ -1701,19 +1777,18 @@
                     AppSearchBatchResult.Builder<String, Void> resultBuilder =
                             new AppSearchBatchResult.Builder<>();
                     instance = mAppSearchUserInstanceManager.getUserInstance(targetUser);
-                    for (int i = 0; i < request.getIds().size(); i++) {
-                        String id = request.getIds().get(i);
+                    for (String id : request.getRemoveByDocumentIdRequest().getIds()) {
                         try {
                             instance.getAppSearchImpl().remove(
                                     callingPackageName,
                                     request.getDatabaseName(),
-                                    request.getNamespace(),
+                                    request.getRemoveByDocumentIdRequest().getNamespace(),
                                     id,
                                     /* removeStatsBuilder= */ null);
                             ++operationSuccessCount;
                             resultBuilder.setSuccess(id, /*result= */ null);
                         } catch (AppSearchException | RuntimeException e) {
-                            // We don't rethrow here so we can still keep trying for the following
+                            // We don't rethrow here, so we can still keep trying for the following
                             // ones.
                             AppSearchResult<Void> result = throwableToFailedResult(e);
                             resultBuilder.setResult(id, result);
@@ -1725,13 +1800,15 @@
                     }
                     // Now that the batch has been written. Persist the newly written data.
                     instance.getAppSearchImpl().persistToDisk(PersistType.Code.LITE);
-                    invokeCallbackOnResult(callback, resultBuilder.build());
+                    invokeCallbackOnResult(callback, AppSearchBatchResultParcel.fromStringToVoid(
+                            resultBuilder.build()));
 
                     // Schedule a task to dispatch change notifications. See requirements for where
                     // the method is called documented in the method description.
                     dispatchChangeNotifications(instance);
 
-                    checkForOptimize(targetUser, instance, request.getIds().size());
+                    checkForOptimize(targetUser, instance,
+                            request.getRemoveByDocumentIdRequest().getIds().size());
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
@@ -1765,7 +1842,8 @@
                 logRateLimitedOrCallDeniedCallStats(callingPackageName, request.getDatabaseName(),
                         CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, targetUser,
                         request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
-                        /* numOperations= */ request.getIds().size(), RESULT_RATE_LIMITED);
+                        /* numOperations= */ request.getRemoveByDocumentIdRequest().getIds().size(),
+                        RESULT_RATE_LIMITED);
             }
         }
 
@@ -1779,8 +1857,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1808,7 +1885,7 @@
                     // Now that the batch has been written. Persist the newly written data.
                     instance.getAppSearchImpl().persistToDisk(PersistType.Code.LITE);
                     ++operationSuccessCount;
-                    invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
 
                     // Schedule a task to dispatch change notifications. See requirements for where
                     // the method is called documented in the method description.
@@ -1819,7 +1896,8 @@
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     // TODO(b/261959320) add outstanding latency fields in AppSearch stats
                     if (instance != null) {
@@ -1862,8 +1940,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -1885,12 +1962,13 @@
                             callingPackageName, request.getDatabaseName());
                     ++operationSuccessCount;
                     invokeCallbackOnResult(
-                            callback, AppSearchResult.newSuccessfulResult(storageInfo));
+                            callback, AppSearchResultParcel.fromStorageInfo(storageInfo));
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
@@ -1930,8 +2008,7 @@
             try {
                 UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
                         request.getCallerAttributionSource(), request.getUserHandle());
-                String callingPackageName = Objects.requireNonNull(
-                        request.getCallerAttributionSource().getPackageName());
+                String callingPackageName = request.getCallerAttributionSource().getPackageName();
                 if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
                         CallStats.CALL_TYPE_FLUSH, targetUser,
                         request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
@@ -2010,14 +2087,13 @@
             try {
                 UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
                         request.getCallerAttributionSource(), request.getUserHandle());
-                callingPackageName = Objects.requireNonNull(
-                        request.getCallerAttributionSource().getPackageName());
+                callingPackageName = request.getCallerAttributionSource().getPackageName();
                 if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
                         CallStats.CALL_TYPE_REGISTER_OBSERVER_CALLBACK, targetUser,
                         request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
                         /* numOperations= */ 1)) {
-                    return new AppSearchResultParcel<>(
-                            AppSearchResult.newFailedResult(RESULT_DENIED, null));
+                    return AppSearchResultParcel.fromFailedResult(AppSearchResult.newFailedResult(
+                            RESULT_DENIED, null));
                 }
                 long callingIdentity = Binder.clearCallingIdentity();
                 try {
@@ -2046,7 +2122,7 @@
                             mExecutorManager.getOrCreateUserExecutor(targetUser),
                             new AppSearchObserverProxy(observerProxyStub));
                     ++operationSuccessCount;
-                    return new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(null));
+                    return AppSearchResultParcel.fromVoid();
                 } finally {
                     Binder.restoreCallingIdentity(callingIdentity);
                 }
@@ -2054,7 +2130,7 @@
                 ++operationFailureCount;
                 AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                 statusCode = failedResult.getResultCode();
-                return new AppSearchResultParcel<>(failedResult);
+                return AppSearchResultParcel.fromFailedResult(failedResult);
             } finally {
                 if (instance != null) {
                     int estimatedBinderLatencyMillis =
@@ -2095,14 +2171,13 @@
             try {
                 UserHandle targetUser = mServiceImplHelper.verifyIncomingCall(
                         request.getCallerAttributionSource(), request.getUserHandle());
-                String callingPackageName = Objects.requireNonNull(
-                        request.getCallerAttributionSource().getPackageName());
+                String callingPackageName = request.getCallerAttributionSource().getPackageName();
                 if (checkCallDenied(callingPackageName, /* callingDatabaseName= */ null,
                         CallStats.CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK, targetUser,
                         request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
                         /* numOperations= */ 1)) {
-                    return new AppSearchResultParcel<>(
-                            AppSearchResult.newFailedResult(RESULT_DENIED, null));
+                    return AppSearchResultParcel.fromFailedResult(AppSearchResult.newFailedResult(
+                            RESULT_DENIED, null));
                 }
                 long callingIdentity = Binder.clearCallingIdentity();
                 try {
@@ -2111,7 +2186,7 @@
                             request.getObservedPackage(),
                             new AppSearchObserverProxy(observerProxyStub));
                     ++operationSuccessCount;
-                    return new AppSearchResultParcel<>(AppSearchResult.newSuccessfulResult(null));
+                    return AppSearchResultParcel.fromVoid();
                 } finally {
                     Binder.restoreCallingIdentity(callingIdentity);
                 }
@@ -2119,7 +2194,7 @@
                 ++operationFailureCount;
                 AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                 statusCode = failedResult.getResultCode();
-                return new AppSearchResultParcel<>(failedResult);
+                return AppSearchResultParcel.fromFailedResult(failedResult);
             } finally {
                 if (instance != null) {
                     int estimatedBinderLatencyMillis =
@@ -2155,8 +2230,7 @@
             long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
             UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
                     request.getCallerAttributionSource(), request.getUserHandle(), callback);
-            String callingPackageName =
-                    Objects.requireNonNull(request.getCallerAttributionSource().getPackageName());
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
             if (targetUser == null) {
                 return;  // Verification failed; verifyIncomingCall triggered callback.
             }
@@ -2164,8 +2238,8 @@
                     CallStats.CALL_TYPE_INITIALIZE)) {
                 // Note: can't log CallStats here since UserInstance isn't guaranteed to (and most
                 // likely does not) exist
-                invokeCallbackOnResult(callback,
-                        AppSearchResult.newFailedResult(RESULT_DENIED, null));
+                invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                        AppSearchResult.newFailedResult(RESULT_DENIED, null)));
                 return;
             }
             mServiceImplHelper.executeLambdaForUserAsync(targetUser, callback, callingPackageName,
@@ -2182,12 +2256,13 @@
                             targetUser,
                             mAppSearchConfig);
                     ++operationSuccessCount;
-                    invokeCallbackOnResult(callback, AppSearchResult.newSuccessfulResult(null));
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromVoid());
                 } catch (AppSearchException | RuntimeException e) {
                     ++operationFailureCount;
                     AppSearchResult<Void> failedResult = throwableToFailedResult(e);
                     statusCode = failedResult.getResultCode();
-                    invokeCallbackOnResult(callback, failedResult);
+                    invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                            failedResult));
                 } finally {
                     if (instance != null) {
                         int estimatedBinderLatencyMillis =
@@ -2212,6 +2287,213 @@
             });
         }
 
+        @Override
+        public void executeAppFunction(
+                @NonNull ExecuteAppFunctionAidlRequest request,
+                @NonNull IAppSearchResultCallback callback) {
+            Objects.requireNonNull(request);
+            Objects.requireNonNull(callback);
+
+            long totalLatencyStartTimeMillis = SystemClock.elapsedRealtime();
+
+            String callingPackageName = request.getCallerAttributionSource().getPackageName();
+            UserHandle targetUser = mServiceImplHelper.verifyIncomingCallWithCallback(
+                    request.getCallerAttributionSource(), request.getUserHandle(), callback);
+            if (targetUser == null) {
+                return;  // Verification failed; verifyIncomingCall triggered callback.
+            }
+            if (checkCallDenied(
+                    callingPackageName, /* databaseName= */ null,
+                    CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION, callback, targetUser,
+                    request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
+                    /* numOperations= */ 1)) {
+                return;
+            }
+
+            // Log the stats as well whenever we invoke the AppSearchResultCallback.
+            final SafeOneTimeAppSearchResultCallback safeCallback =
+                    new SafeOneTimeAppSearchResultCallback(callback, result -> {
+                        AppSearchUserInstance instance =
+                                mAppSearchUserInstanceManager.getUserInstance(targetUser);
+                        int totalLatencyMillis =
+                                (int) (SystemClock.elapsedRealtime() - totalLatencyStartTimeMillis);
+                        int estimatedBinderLatencyMillis =
+                                2 * (int) (totalLatencyStartTimeMillis
+                                        - request.getBinderCallStartTimeMillis());
+                        instance.getLogger().logStats(new CallStats.Builder()
+                                .setPackageName(callingPackageName)
+                                .setStatusCode(result.getResultCode())
+                                .setTotalLatencyMillis(totalLatencyMillis)
+                                .setCallType(CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION)
+                                .setEstimatedBinderLatencyMillis(estimatedBinderLatencyMillis)
+                                .build());
+                    });
+
+            // TODO(b/327134039): Add a new policy for this in W timeframe.
+            if (mServiceImplHelper.isUserOrganizationManaged(targetUser)) {
+                safeCallback.onFailedResult(AppSearchResult.newFailedResult(
+                        RESULT_SECURITY_ERROR,
+                        "Cannot run on a device with a device owner or from the managed profile."));
+                return;
+            }
+
+            String targetPackageName = request.getClientRequest().getTargetPackageName();
+            if (TextUtils.isEmpty(targetPackageName)) {
+                safeCallback.onFailedResult(AppSearchResult.newFailedResult(
+                        RESULT_INVALID_ARGUMENT,
+                        "targetPackageName cannot be empty."));
+                return;
+            }
+            if (!verifyExecuteAppFunctionCaller(
+                    callingPackageName,
+                    targetPackageName,
+                    targetUser)) {
+                safeCallback.onFailedResult(AppSearchResult.newFailedResult(
+                        RESULT_SECURITY_ERROR,
+                        callingPackageName + " is not allowed to call executeAppFunction"));
+                return;
+            }
+
+            boolean callAccepted = mServiceImplHelper.executeLambdaForUserAsync(
+                    targetUser, callback, callingPackageName,
+                    CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION,
+                    () -> executeAppFunctionUnchecked(
+                            request.getClientRequest(),
+                            targetUser,
+                            safeCallback));
+            if (!callAccepted) {
+                logRateLimitedOrCallDeniedCallStats(callingPackageName, /* databaseName= */ null,
+                        CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION, targetUser,
+                        request.getBinderCallStartTimeMillis(), totalLatencyStartTimeMillis,
+                        /*numOperations=*/ 1, RESULT_RATE_LIMITED);
+            }
+        }
+
+        /**
+         * The same as {@link #executeAppFunction}, except this is without the caller check.
+         * This method runs on the user-local thread pool.
+         */
+        @WorkerThread
+        private void executeAppFunctionUnchecked(
+                @NonNull ExecuteAppFunctionRequest request,
+                @NonNull UserHandle userHandle,
+                @NonNull SafeOneTimeAppSearchResultCallback safeCallback) {
+            Intent serviceIntent = new Intent(AppFunctionService.SERVICE_INTERFACE);
+            serviceIntent.setPackage(request.getTargetPackageName());
+
+            Context userContext = mAppSearchEnvironment.createContextAsUser(mContext, userHandle);
+            ResolveInfo resolveInfo = userContext.getPackageManager()
+                    .resolveService(serviceIntent, 0);
+            if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+                safeCallback.onFailedResult(AppSearchResult.newFailedResult(
+                        RESULT_NOT_FOUND, "Cannot find the target service."));
+                return;
+            }
+            ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            if (!PERMISSION_BIND_APP_FUNCTION_SERVICE.equals(serviceInfo.permission)) {
+                safeCallback.onFailedResult(AppSearchResult.newFailedResult(
+                        RESULT_NOT_FOUND,
+                        "Failed to find a valid target service. The resolved service is missing "
+                                + "the BIND_APP_FUNCTION_SERVICE permission."));
+                return;
+            }
+            serviceIntent.setComponent(
+                    new ComponentName(serviceInfo.packageName, serviceInfo.name));
+
+            if (request.getSha256Certificate() != null) {
+                if (!PackageManagerUtil.hasSigningCertificate(
+                        mContext, request.getTargetPackageName(), request.getSha256Certificate())) {
+                    safeCallback.onFailedResult(
+                            AppSearchResult.newFailedResult(
+                                    RESULT_NOT_FOUND, "Cannot find the target service"));
+                    return;
+                }
+            }
+
+            boolean bindServiceResult = mAppFunctionServiceCallHelper.runServiceCall(
+                    serviceIntent,
+                    Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS | Context.BIND_AUTO_CREATE,
+                    mAppSearchConfig.getAppFunctionCallTimeoutMillis(),
+                    userHandle,
+                    new ServiceCallHelper.RunServiceCallCallback<>() {
+                        @Override
+                        public void onServiceConnected(
+                                @NonNull IAppFunctionService service,
+                                @NonNull ServiceUsageCompleteListener completeListener) {
+                            try {
+                                service.executeAppFunction(
+                                        request,
+                                        new IAppSearchResultCallback.Stub() {
+                                            @Override
+                                            public void onResult(
+                                                    AppSearchResultParcel resultParcel) {
+                                                safeCallback.onResult(resultParcel);
+                                                completeListener.onCompleted();
+                                            }
+                                        });
+                            } catch (Exception e) {
+                                safeCallback.onFailedResult(AppSearchResult
+                                        .throwableToFailedResult(e));
+                                completeListener.onCompleted();
+                            }
+                        }
+
+                        @Override
+                        public void onFailedToConnect() {
+                            safeCallback.onFailedResult(
+                                    AppSearchResult.newFailedResult(RESULT_INTERNAL_ERROR, null));
+                        }
+
+                        @Override
+                        public void onTimedOut() {
+                            safeCallback.onFailedResult(
+                                    AppSearchResult.newFailedResult(RESULT_TIMED_OUT, null));
+                        }
+                    });
+            if (!bindServiceResult) {
+                safeCallback.onFailedResult(AppSearchResult.newFailedResult(
+                        RESULT_INTERNAL_ERROR, "Failed to bind the target service."));
+            }
+        }
+
+        /**
+         * Determines whether the caller is authorized to execute an app function via
+         * {@link #executeAppFunction}.
+         * <p>
+         * Authorization is granted under the following conditions:
+         * <ul>
+         *     <li>The caller is the same app that owns the target function.</li>
+         *     <li>The caller possesses the SYSTEM_UI_INTELLIGENCE role for the target user. </li>
+         * </ul>
+         *
+         * @param callingPackage The validated package name of the calling app.
+         * @param targetPackage  The package name of the target app.
+         * @param targetUser     The target user.
+         * @return               {@code true} if the caller is authorized, {@code false} otherwise.
+         */
+        private boolean verifyExecuteAppFunctionCaller(
+                @NonNull String callingPackage,
+                @NonNull String targetPackage,
+                @NonNull UserHandle targetUser) {
+            // While adding new system role-based permissions through mainline updates is possible,
+            // granting them to system apps in previous android versions is not. System apps must
+            // request permissions in their prebuilt APKs included in the system image. We cannot
+            // modify prebuilts in older images anymore.
+            // TODO(b/327134039): Enforce permission checking for Android V+ or W+, depending on
+            // whether the new prebuilt can be included in the system image on time.
+            if (callingPackage.equals(targetPackage)) {
+                return true;
+            }
+            long originalToken = Binder.clearCallingIdentity();
+            try {
+                List<String> systemUiIntelligencePackages =
+                        mRoleManager.getRoleHoldersAsUser(SYSTEM_UI_INTELLIGENCE, targetUser);
+                return systemUiIntelligencePackages.contains(callingPackage);
+            } finally {
+                Binder.restoreCallingIdentity(originalToken);
+            }
+        }
+
         @BinderThread
         private void dumpContactsIndexer(@NonNull PrintWriter pw, boolean verbose) {
             Objects.requireNonNull(pw);
@@ -2285,13 +2567,13 @@
             if (args != null) {
                 for (int i = 0; i < args.length; i++) {
                     String arg = args[i];
-                    if ("-h".equals(arg)) {
+                    if (Objects.equals(arg, "-h")) {
                         pw.println(
                                 "Dumps the internal state of AppSearch platform storage and "
                                         + "AppSearch Contacts Indexer for the current user.");
                         pw.println("-v, verbose mode");
                         return;
-                    } else if ("-v".equals(arg) || "-a".equals(arg)) {
+                    } else if (Objects.equals(arg, "-v") || Objects.equals(arg, "-a")) {
                         // "-a" is included when adb dumps all services e.g. in adb bugreport so we
                         // want to run in verbose mode when this happens
                         verbose = true;
@@ -2567,7 +2849,8 @@
             long binderCallStartTimeMillis, long totalLatencyStartTimeMillis, int numOperations) {
         if (checkCallDenied(callingPackageName, callingDatabaseName, apiType, targetUser,
                 binderCallStartTimeMillis, totalLatencyStartTimeMillis, numOperations)) {
-            invokeCallbackOnResult(callback, AppSearchResult.newFailedResult(RESULT_DENIED, null));
+            invokeCallbackOnResult(callback, AppSearchResultParcel.fromFailedResult(
+                    AppSearchResult.newFailedResult(RESULT_DENIED, null)));
             return true;
         }
         return false;
diff --git a/service/java/com/android/server/appsearch/AppSearchModule.java b/service/java/com/android/server/appsearch/AppSearchModule.java
index 52f2df7..e5a1bf8 100644
--- a/service/java/com/android/server/appsearch/AppSearchModule.java
+++ b/service/java/com/android/server/appsearch/AppSearchModule.java
@@ -16,39 +16,80 @@
 
 package com.android.server.appsearch;
 
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.APPS_INDEXER;
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.CONTACTS_INDEXER;
+
 import android.annotation.BinderThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.appsearch.util.ExceptionUtil;
+import android.app.appsearch.util.LogUtil;
 import android.content.Context;
 import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.SystemService;
+import com.android.server.appsearch.appsindexer.AppsIndexerConfig;
+import com.android.server.appsearch.appsindexer.AppsIndexerManagerService;
+import com.android.server.appsearch.appsindexer.FrameworkAppsIndexerConfig;
 import com.android.server.appsearch.contactsindexer.ContactsIndexerConfig;
-import com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService;
-import com.android.server.appsearch.contactsindexer.FrameworkContactsIndexerConfig;
 import com.android.server.appsearch.contactsindexer.ContactsIndexerManagerService;
-import com.android.server.appsearch.util.ExceptionUtil;
+import com.android.server.appsearch.contactsindexer.FrameworkContactsIndexerConfig;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
 
 import java.io.PrintWriter;
 import java.util.Objects;
 
+/** This class encapsulate the lifecycle methods of AppSearch module. */
 public class AppSearchModule {
     private static final String TAG = "AppSearchModule";
 
-    public static final class Lifecycle extends SystemService {
+    /** Lifecycle definition for AppSearch module. */
+    public static class Lifecycle extends SystemService {
         private AppSearchManagerService mAppSearchManagerService;
-        @Nullable
-        private ContactsIndexerManagerService mContactsIndexerManagerService;
+        @VisibleForTesting @Nullable ContactsIndexerManagerService mContactsIndexerManagerService;
+
+        @VisibleForTesting @Nullable AppsIndexerManagerService mAppsIndexerManagerService;
 
         public Lifecycle(Context context) {
             super(context);
         }
 
+        /** Added primarily for testing purposes. */
+        @VisibleForTesting
+        @NonNull
+        AppSearchManagerService createAppSearchManagerService(
+                @NonNull Context context, @NonNull AppSearchModule.Lifecycle lifecycle) {
+            Objects.requireNonNull(context);
+            Objects.requireNonNull(lifecycle);
+            return new AppSearchManagerService(context, lifecycle);
+        }
+
+        /** Added primarily for testing purposes. */
+        @VisibleForTesting
+        @NonNull
+        AppsIndexerManagerService createAppsIndexerManagerService(
+                @NonNull Context context, @NonNull AppsIndexerConfig config) {
+            Objects.requireNonNull(context);
+            Objects.requireNonNull(config);
+            return new AppsIndexerManagerService(context, config);
+        }
+
+        /** Added primarily for testing purposes. */
+        @VisibleForTesting
+        @NonNull
+        ContactsIndexerManagerService createContactsIndexerManagerService(
+                @NonNull Context context, @NonNull ContactsIndexerConfig config) {
+            Objects.requireNonNull(context);
+            Objects.requireNonNull(config);
+            return new ContactsIndexerManagerService(context, config);
+        }
+
         @Override
         public void onStart() {
-            mAppSearchManagerService = new AppSearchManagerService(
-                    getContext(), /* lifecycle= */ this);
+            mAppSearchManagerService =
+                    createAppSearchManagerService(getContext(), /* lifecycle= */ this);
 
             try {
                 mAppSearchManagerService.onStart();
@@ -64,8 +105,9 @@
             // uses, starts before AppSearch.
             ContactsIndexerConfig contactsIndexerConfig = new FrameworkContactsIndexerConfig();
             if (contactsIndexerConfig.isContactsIndexerEnabled()) {
-                mContactsIndexerManagerService = new ContactsIndexerManagerService(getContext(),
-                        contactsIndexerConfig);
+
+                mContactsIndexerManagerService =
+                        createContactsIndexerManagerService(getContext(), contactsIndexerConfig);
                 try {
                     mContactsIndexerManagerService.onStart();
                 } catch (Throwable t) {
@@ -74,9 +116,23 @@
                     // system_server restart on a device reboot.
                     mContactsIndexerManagerService = null;
                 }
-            } else {
+            } else if (LogUtil.INFO) {
                 Log.i(TAG, "ContactsIndexer service is disabled.");
             }
+
+            AppsIndexerConfig appsIndexerConfig = new FrameworkAppsIndexerConfig();
+            if (appsIndexerConfig.isAppsIndexerEnabled()) {
+                mAppsIndexerManagerService =
+                        createAppsIndexerManagerService(getContext(), appsIndexerConfig);
+                try {
+                    mAppsIndexerManagerService.onStart();
+                } catch (Throwable t) {
+                    Log.e(TAG, "Failed to start AppsIndexer service", t);
+                    mAppsIndexerManagerService = null;
+                }
+            } else if (LogUtil.INFO) {
+                Log.i(TAG, "AppsIndexer service is disabled.");
+            }
         }
 
         /** Dumps ContactsIndexer internal state for the user. */
@@ -90,6 +146,15 @@
             }
         }
 
+        @BinderThread
+        void dumpAppsIndexerForUser(@NonNull UserHandle userHandle, @NonNull PrintWriter pw) {
+            if (mAppsIndexerManagerService != null) {
+                mAppsIndexerManagerService.dumpAppsIndexerForUser(userHandle, pw);
+            } else {
+                pw.println("No dumpsys for AppsIndexer as it is disabled");
+            }
+        }
+
         @Override
         public void onBootPhase(int phase) {
             mAppSearchManagerService.onBootPhase(phase);
@@ -99,11 +164,18 @@
         public void onUserUnlocking(@NonNull TargetUser user) {
             mAppSearchManagerService.onUserUnlocking(user);
             if (mContactsIndexerManagerService == null) {
-                ContactsIndexerMaintenanceService.cancelFullUpdateJobIfScheduled(getContext(),
-                        user.getUserHandle());
+                IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                        getContext(), user.getUserHandle(), CONTACTS_INDEXER);
             } else {
                 mContactsIndexerManagerService.onUserUnlocking(user);
             }
+
+            if (mAppsIndexerManagerService == null) {
+                IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                        getContext(), user.getUserHandle(), APPS_INDEXER);
+            } else {
+                mAppsIndexerManagerService.onUserUnlocking(user);
+            }
         }
 
         @Override
@@ -112,6 +184,9 @@
             if (mContactsIndexerManagerService != null) {
                 mContactsIndexerManagerService.onUserStopping(user);
             }
+            if (mAppsIndexerManagerService != null) {
+                mAppsIndexerManagerService.onUserStopping(user);
+            }
         }
     }
 }
diff --git a/service/java/com/android/server/appsearch/AppSearchRateLimitConfig.java b/service/java/com/android/server/appsearch/AppSearchRateLimitConfig.java
index 3c442e2..cfbf214 100644
--- a/service/java/com/android/server/appsearch/AppSearchRateLimitConfig.java
+++ b/service/java/com/android/server/appsearch/AppSearchRateLimitConfig.java
@@ -38,45 +38,38 @@
  * capacity.
  *
  * <p>Each AppSearch API call has an associated integer cost that is configured by the API costs
- * string. API costs must be positive.
- * The API costs string uses API_ENTRY_DELIMITER (';') to separate API entries and has a string API
- * name followed by API_COST_DELIMITER (':') and the integer cost to define each entry.
- * If an API's cost is not specified in the string, its cost is set to DEFAULT_API_COST.
- * e.g. A valid API cost string: "putDocument:5;query:1;setSchema:10".
+ * string. API costs must be positive. The API costs string uses API_ENTRY_DELIMITER (';') to
+ * separate API entries and has a string API name followed by API_COST_DELIMITER (':') and the
+ * integer cost to define each entry. If an API's cost is not specified in the string, its cost is
+ * set to DEFAULT_API_COST. e.g. A valid API cost string: "putDocument:5;query:1;setSchema:10".
  *
  * <p>If an API call has a higher cost, this means that the API consumes more of the task queue
- * budget and fewer number of tasks can be placed on the task queue.
- * An incoming API call from a calling package is dropped when the rate limit is exceeded, which
- * happens when either:
- * 1. Total cost of all API calls currently on the task queue + cost of incoming API call >
- * task queue total capacity. OR
- * 2. Total cost of all API calls currently on the task queue from the calling package +
- * cost of incoming API call > task queue per-package capacity.
+ * budget and fewer number of tasks can be placed on the task queue. An incoming API call from a
+ * calling package is dropped when the rate limit is exceeded, which happens when either: 1. Total
+ * cost of all API calls currently on the task queue + cost of incoming API call > task queue total
+ * capacity. OR 2. Total cost of all API calls currently on the task queue from the calling package
+ * + cost of incoming API call > task queue per-package capacity.
  */
 public final class AppSearchRateLimitConfig {
-    @VisibleForTesting
-    public static final int DEFAULT_API_COST = 1;
+    @VisibleForTesting public static final int DEFAULT_API_COST = 1;
 
     /**
      * Creates an instance of {@link AppSearchRateLimitConfig}.
      *
-     * @param totalCapacity                configures total cost of tasks that AppSearch can accept
-     *                                     onto its task queue from all packages.
+     * @param totalCapacity configures total cost of tasks that AppSearch can accept onto its task
+     *     queue from all packages.
      * @param perPackageCapacityPercentage configures total cost of tasks that AppSearch can accept
-     *                                     onto its task queue from a single calling package, as a
-     *                                     percentage of totalCapacity.
-     * @param apiCostsString               configures costs for each {@link CallStats.CallType}. The
-     *                                     string should use API_ENTRY_DELIMITER (';') to separate
-     *                                     entries, with each entry defined by the string API name
-     *                                     followed by API_COST_DELIMITER (':').
-     *                                     e.g. "putDocument:5;query:1;setSchema:10"
+     *     onto its task queue from a single calling package, as a percentage of totalCapacity.
+     * @param apiCostsString configures costs for each {@link CallStats.CallType}. The string should
+     *     use API_ENTRY_DELIMITER (';') to separate entries, with each entry defined by the string
+     *     API name followed by API_COST_DELIMITER (':'). e.g. "putDocument:5;query:1;setSchema:10"
      */
-    public static AppSearchRateLimitConfig create(int totalCapacity,
-            float perPackageCapacityPercentage, @NonNull String apiCostsString) {
+    public static AppSearchRateLimitConfig create(
+            int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString) {
         Objects.requireNonNull(apiCostsString);
         Map<Integer, Integer> apiCostsMap = createApiCostsMap(apiCostsString);
-        return new AppSearchRateLimitConfig(totalCapacity, perPackageCapacityPercentage,
-                apiCostsString, apiCostsMap);
+        return new AppSearchRateLimitConfig(
+                totalCapacity, perPackageCapacityPercentage, apiCostsString, apiCostsMap);
     }
 
     // Truncated as logging tag is allowed to be at most 23 characters.
@@ -91,8 +84,11 @@
     // Mapping of @CallStats.CallType -> cost
     private final Map<Integer, Integer> mTaskQueueApiCosts;
 
-    private AppSearchRateLimitConfig(int totalCapacity, float perPackageCapacityPercentage,
-            @NonNull String apiCostsString, @NonNull Map<Integer, Integer> apiCostsMap) {
+    private AppSearchRateLimitConfig(
+            int totalCapacity,
+            float perPackageCapacityPercentage,
+            @NonNull String apiCostsString,
+            @NonNull Map<Integer, Integer> apiCostsMap) {
         mTaskQueueTotalCapacity = totalCapacity;
         mTaskQueuePerPackageCapacity = (int) (totalCapacity * perPackageCapacityPercentage);
         mApiCostsString = Objects.requireNonNull(apiCostsString);
@@ -100,48 +96,39 @@
     }
 
     /**
-     * Returns an AppSearchRateLimitConfig instance given the input capacities and ApiCosts.
-     * This may be the same instance if there are no changes in these configs.
+     * Returns an AppSearchRateLimitConfig instance given the input capacities and ApiCosts. This
+     * may be the same instance if there are no changes in these configs.
      *
-     * @param totalCapacity                configures total cost of tasks that AppSearch can accept
-     *                                     onto its task queue from all packages.
+     * @param totalCapacity configures total cost of tasks that AppSearch can accept onto its task
+     *     queue from all packages.
      * @param perPackageCapacityPercentage configures total cost of tasks that AppSearch can accept
-     *                                     onto its task queue from a single calling package, as a
-     *                                     percentage of totalCapacity.
-     * @param apiCostsString               configures costs for each {@link CallStats.CallType}. The
-     *                                     string should use API_ENTRY_DELIMITER (';') to separate
-     *                                     entries, with each entry defined by the string API name
-     *                                     followed by API_COST_DELIMITER (':').
-     *                                     e.g. "putDocument:5;query:1;setSchema:10"
+     *     onto its task queue from a single calling package, as a percentage of totalCapacity.
+     * @param apiCostsString configures costs for each {@link CallStats.CallType}. The string should
+     *     use API_ENTRY_DELIMITER (';') to separate entries, with each entry defined by the string
+     *     API name followed by API_COST_DELIMITER (':'). e.g. "putDocument:5;query:1;setSchema:10"
      */
-    public AppSearchRateLimitConfig rebuildIfNecessary(int totalCapacity,
-            float perPackageCapacityPercentage, @NonNull String apiCostsString) {
+    public AppSearchRateLimitConfig rebuildIfNecessary(
+            int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString) {
         int perPackageCapacity = (int) (totalCapacity * perPackageCapacityPercentage);
         if (totalCapacity != mTaskQueueTotalCapacity
                 || perPackageCapacity != mTaskQueuePerPackageCapacity
                 || !Objects.equals(apiCostsString, mApiCostsString)) {
-            return AppSearchRateLimitConfig.create(totalCapacity, perPackageCapacityPercentage,
-                    apiCostsString);
+            return AppSearchRateLimitConfig.create(
+                    totalCapacity, perPackageCapacityPercentage, apiCostsString);
         }
         return this;
     }
 
-    /**
-     * Returns the task queue total capacity.
-     */
+    /** Returns the task queue total capacity. */
     public int getTaskQueueTotalCapacity() {
         return mTaskQueueTotalCapacity;
     }
 
-
-    /**
-     * Returns the per-package task queue capacity.
-     */
+    /** Returns the per-package task queue capacity. */
     public int getTaskQueuePerPackageCapacity() {
         return mTaskQueuePerPackageCapacity;
     }
 
-
     /**
      * Returns the cost of an API type.
      *
@@ -152,9 +139,7 @@
         return mTaskQueueApiCosts.getOrDefault(apiType, DEFAULT_API_COST);
     }
 
-    /**
-     * Returns an API costs map based on apiCostsString.
-     */
+    /** Returns an API costs map based on apiCostsString. */
     private static Map<Integer, Integer> createApiCostsMap(@NonNull String apiCostsString) {
         if (TextUtils.getTrimmedLength(apiCostsString) == 0) {
             return new ArrayMap<>();
@@ -171,8 +156,9 @@
             String apiName = entry.substring(0, costDelimiterIndex);
             int apiCost;
             try {
-                apiCost = Integer.parseInt(entry, costDelimiterIndex + 1,
-                        entry.length(), /* radix= */10);
+                apiCost =
+                        Integer.parseInt(
+                                entry, costDelimiterIndex + 1, entry.length(), /* radix= */ 10);
             } catch (NumberFormatException e) {
                 Log.e(TAG, "Invalid cost for API cost entry: " + entry);
                 continue;
diff --git a/service/java/com/android/server/appsearch/AppSearchUserInstanceManager.java b/service/java/com/android/server/appsearch/AppSearchUserInstanceManager.java
index eec1f2b..6396943 100644
--- a/service/java/com/android/server/appsearch/AppSearchUserInstanceManager.java
+++ b/service/java/com/android/server/appsearch/AppSearchUserInstanceManager.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.app.appsearch.AppSearchEnvironmentFactory;
 import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.util.LogUtil;
 import android.content.Context;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -50,6 +51,7 @@
 
     @GuardedBy("mInstancesLocked")
     private final Map<UserHandle, AppSearchUserInstance> mInstancesLocked = new ArrayMap<>();
+
     @GuardedBy("mStorageInfoLocked")
     private final Map<UserHandle, UserStorageInfo> mStorageInfoLocked = new ArrayMap<>();
 
@@ -88,7 +90,7 @@
     public AppSearchUserInstance getOrCreateUserInstance(
             @NonNull Context userContext,
             @NonNull UserHandle userHandle,
-            @NonNull FrameworkAppSearchConfig config)
+            @NonNull ServiceAppSearchConfig config)
             throws AppSearchException {
         Objects.requireNonNull(userContext);
         Objects.requireNonNull(userHandle);
@@ -133,7 +135,7 @@
      * @param userHandle The multi-user handle of the device user calling AppSearch
      * @return An initialized {@link AppSearchUserInstance} for this user
      * @throws IllegalStateException if {@link AppSearchUserInstance} haven't created for the given
-     *                               user.
+     *     user.
      */
     @NonNull
     public AppSearchUserInstance getUserInstance(@NonNull UserHandle userHandle) {
@@ -174,15 +176,15 @@
      */
     @NonNull
     public UserStorageInfo getOrCreateUserStorageInfoInstance(
-        @NonNull Context userContext, @NonNull UserHandle userHandle) {
+            @NonNull Context userContext, @NonNull UserHandle userHandle) {
         Objects.requireNonNull(userContext);
         Objects.requireNonNull(userHandle);
         synchronized (mStorageInfoLocked) {
             UserStorageInfo userStorageInfo = mStorageInfoLocked.get(userHandle);
             if (userStorageInfo == null) {
-                File appSearchDir = AppSearchEnvironmentFactory
-                    .getEnvironmentInstance()
-                    .getAppSearchDir(userContext, userHandle);
+                File appSearchDir =
+                        AppSearchEnvironmentFactory.getEnvironmentInstance()
+                                .getAppSearchDir(userContext, userHandle);
                 userStorageInfo = new UserStorageInfo(appSearchDir);
                 mStorageInfoLocked.put(userHandle, userStorageInfo);
             }
@@ -206,37 +208,39 @@
     private AppSearchUserInstance createUserInstance(
             @NonNull Context userContext,
             @NonNull UserHandle userHandle,
-            @NonNull FrameworkAppSearchConfig config)
+            @NonNull ServiceAppSearchConfig config)
             throws AppSearchException {
         long totalLatencyStartMillis = SystemClock.elapsedRealtime();
         InitializeStats.Builder initStatsBuilder = new InitializeStats.Builder();
 
         // Initialize the classes that make up AppSearchUserInstance
-        InternalAppSearchLogger logger = AppSearchComponentFactory
-                .createLoggerInstance(userContext, config);
+        InternalAppSearchLogger logger =
+                AppSearchComponentFactory.createLoggerInstance(userContext, config);
 
-        File appSearchDir = AppSearchEnvironmentFactory
-            .getEnvironmentInstance()
-            .getAppSearchDir(userContext, userHandle);
+        File appSearchDir =
+                AppSearchEnvironmentFactory.getEnvironmentInstance()
+                        .getAppSearchDir(userContext, userHandle);
         File icingDir = new File(appSearchDir, "icing");
-        Log.i(TAG, "Creating new AppSearch instance at: " + icingDir);
-        VisibilityChecker visibilityCheckerImpl = AppSearchComponentFactory
-                .createVisibilityCheckerInstance(userContext);
-        AppSearchImpl appSearchImpl = AppSearchImpl.create(
-                icingDir,
-                config,
-                initStatsBuilder,
-                visibilityCheckerImpl,
-                new FrameworkOptimizeStrategy(config));
+        if (LogUtil.INFO) {
+            Log.i(TAG, "Creating new AppSearch instance at: " + icingDir);
+        }
+        VisibilityChecker visibilityCheckerImpl =
+                AppSearchComponentFactory.createVisibilityCheckerInstance(userContext);
+        AppSearchImpl appSearchImpl =
+                AppSearchImpl.create(
+                        icingDir,
+                        config,
+                        initStatsBuilder,
+                        visibilityCheckerImpl,
+                        new ServiceOptimizeStrategy(config));
 
         // Update storage info file
-        UserStorageInfo userStorageInfo = getOrCreateUserStorageInfoInstance(
-            userContext, userHandle);
+        UserStorageInfo userStorageInfo =
+                getOrCreateUserStorageInfoInstance(userContext, userHandle);
         userStorageInfo.updateStorageInfoFile(appSearchImpl);
 
-        initStatsBuilder
-                .setTotalLatencyMillis(
-                        (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
+        initStatsBuilder.setTotalLatencyMillis(
+                (int) (SystemClock.elapsedRealtime() - totalLatencyStartMillis));
         logger.logStats(initStatsBuilder.build());
 
         return new AppSearchUserInstance(logger, appSearchImpl, visibilityCheckerImpl);
diff --git a/service/java/com/android/server/appsearch/Denylist.java b/service/java/com/android/server/appsearch/Denylist.java
index 3989fe4..6a3818b 100644
--- a/service/java/com/android/server/appsearch/Denylist.java
+++ b/service/java/com/android/server/appsearch/Denylist.java
@@ -43,53 +43,55 @@
  *
  * <p>Each entry can only contain the following keys in the format of URL parameters. Unknown keys
  * invalidate the entry in which they're found but do not invalidate other entries.
+ *
  * <ul>
- * <li>pkg - a calling package name
- * <li>db - a calling database name
- * <li>apis - a non-empty, comma-separated list of apis to deny
+ *   <li>pkg - a calling package name
+ *   <li>db - a calling database name
+ *   <li>apis - a non-empty, comma-separated list of apis to deny
  * </ul>
  *
- * <p>At least one of pkg or db must be specified, and consequently, the listed apis will be
- * denied either by calling package, calling database, or the combination of both. Note that
- * a key that is present without a value (i.e. "pkg&..." or "pkg=&...") is not a missing key. For
- * example,
+ * <p>At least one of pkg or db must be specified, and consequently, the listed apis will be denied
+ * either by calling package, calling database, or the combination of both. Note that a key that is
+ * present without a value (i.e. "pkg&..." or "pkg=&...") is not a missing key. For example,
+ *
  * <ul>
- * <li>"pkg=foo&apis=localSetSchema,globalSearch" denies for calling package "foo" and any
- * calling database since db is missing
- * <li>"db=bar&apis=localGetSchema,localGetDocuments" denies for calling database "bar" and
- * any calling package since pkg is missing
- * <li>"pkg=foo&db=bar&apis=localPutDocuments,localSearch" denies only if the calling package is
- * "foo" and the calling database is "bar"
- * <li>"pkg&db=&apis=localReportUsage" denies only if the calling package is "" and the calling
- * database is ""
+ *   <li>"pkg=foo&apis=localSetSchema,globalSearch" denies for calling package "foo" and any calling
+ *       database since db is missing
+ *   <li>"db=bar&apis=localGetSchema,localGetDocuments" denies for calling database "bar" and any
+ *       calling package since pkg is missing
+ *   <li>"pkg=foo&db=bar&apis=localPutDocuments,localSearch" denies only if the calling package is
+ *       "foo" and the calling database is "bar"
+ *   <li>"pkg&db=&apis=localReportUsage" denies only if the calling package is "" and the calling
+ *       database is ""
  * </ul>
  *
  * <p>The full list of apis is:
+ *
  * <ul>
- * <li>initialize
- * <li>localSetSchema
- * <li>localPutDocuments
- * <li>globalGetDocuments
- * <li>localGetDocuments
- * <li>localRemoveByDocumentId
- * <li>localRemoveBySearch
- * <li>globalSearch
- * <li>localSearch
- * <li>flush
- * <li>globalGetSchema
- * <li>localGetSchema
- * <li>localGetNamespaces
- * <li>globalGetNextPage
- * <li>localGetNextPage
- * <li>invalidateNextPageToken
- * <li>localWriteSearchResultsToFile
- * <li>localPutDocumentsFromFile
- * <li>localSearchSuggestion
- * <li>globalReportUsage
- * <li>localReportUsage
- * <li>localGetStorageInfo
- * <li>globalRegisterObserverCallback
- * <li>globalUnregisterObserverCallback
+ *   <li>initialize
+ *   <li>localSetSchema
+ *   <li>localPutDocuments
+ *   <li>globalGetDocuments
+ *   <li>localGetDocuments
+ *   <li>localRemoveByDocumentId
+ *   <li>localRemoveBySearch
+ *   <li>globalSearch
+ *   <li>localSearch
+ *   <li>flush
+ *   <li>globalGetSchema
+ *   <li>localGetSchema
+ *   <li>localGetNamespaces
+ *   <li>globalGetNextPage
+ *   <li>localGetNextPage
+ *   <li>invalidateNextPageToken
+ *   <li>localWriteSearchResultsToFile
+ *   <li>localPutDocumentsFromFile
+ *   <li>localSearchSuggestion
+ *   <li>globalReportUsage
+ *   <li>localReportUsage
+ *   <li>localGetStorageInfo
+ *   <li>globalRegisterObserverCallback
+ *   <li>globalUnregisterObserverCallback
  * </ul>
  *
  * <p>Note, the denylist string is case-sensitive, and whitespace is not trimmed during parsing.
@@ -112,19 +114,16 @@
     private static final String KEY_PACKAGE = "pkg";
     private static final String KEY_DATABASE = "db";
     private static final String KEY_APIS = "apis";
-    private static final Set<String> KNOWN_KEYS = new ArraySet<>(
-            Arrays.asList(KEY_PACKAGE, KEY_DATABASE, KEY_APIS));
+    private static final Set<String> KNOWN_KEYS =
+            new ArraySet<>(Arrays.asList(KEY_PACKAGE, KEY_DATABASE, KEY_APIS));
 
     private final Map<String, Set<Integer>> deniedPackages = new ArrayMap<>();
     private final Map<String, Set<Integer>> deniedDatabases = new ArrayMap<>();
     private final Map<String, Set<Integer>> deniedPrefixes = new ArrayMap<>();
 
-    private Denylist() {
-    }
+    private Denylist() {}
 
-    /**
-     * Creates an instance of {@link Denylist}.
-     */
+    /** Creates an instance of {@link Denylist}. */
     @NonNull
     public static Denylist create(@NonNull String denylistString) {
         Objects.requireNonNull(denylistString);
@@ -152,15 +151,21 @@
             String packageName = uri.getQueryParameter(KEY_PACKAGE);
             String databaseName = uri.getQueryParameter(KEY_DATABASE);
             if (packageName == null && databaseName == null) {
-                Log.e(TAG, "The parameters 'pkg' and 'db' were both missing for this entry: "
-                        + entry);
+                Log.e(
+                        TAG,
+                        "The parameters 'pkg' and 'db' were both missing for this entry: " + entry);
                 continue;
             }
             if (!keys.contains(KEY_APIS)) {
                 Log.e(TAG, "The parameter 'apis' was missing for this entry: " + entry);
                 continue;
             }
-            String[] apis = uri.getQueryParameter(KEY_APIS).split(VALUE_DELIMITER);
+            String queryParameter = uri.getQueryParameter(KEY_APIS);
+            if (queryParameter == null) {
+                Log.e(TAG, "There were no valid api types for this entry: " + entry);
+                continue;
+            }
+            String[] apis = queryParameter.split(VALUE_DELIMITER);
             Set<Integer> apiTypes = retrieveApiTypes(apis);
             if (apiTypes.isEmpty()) {
                 Log.e(TAG, "There were no valid api types for this entry: " + entry);
@@ -182,7 +187,9 @@
         return apiTypes;
     }
 
-    private void addEntry(@Nullable String packageName, @Nullable String databaseName,
+    private void addEntry(
+            @Nullable String packageName,
+            @Nullable String databaseName,
             @NonNull Set<Integer> apiTypes) {
         if (packageName != null && databaseName != null) {
             String prefix = PrefixUtil.createPrefix(packageName, databaseName);
@@ -190,12 +197,12 @@
                     deniedPrefixes.computeIfAbsent(prefix, k -> new ArraySet<>());
             deniedApiTypes.addAll(apiTypes);
         } else if (packageName != null) {
-            Set<Integer> deniedApiTypes = deniedPackages.computeIfAbsent(packageName,
-                    k -> new ArraySet<>());
+            Set<Integer> deniedApiTypes =
+                    deniedPackages.computeIfAbsent(packageName, k -> new ArraySet<>());
             deniedApiTypes.addAll(apiTypes);
         } else if (databaseName != null) {
-            Set<Integer> deniedApiTypes = deniedDatabases.computeIfAbsent(databaseName,
-                    k -> new ArraySet<>());
+            Set<Integer> deniedApiTypes =
+                    deniedDatabases.computeIfAbsent(databaseName, k -> new ArraySet<>());
             deniedApiTypes.addAll(apiTypes);
         }
     }
@@ -209,10 +216,12 @@
      * @param apiType the api type to check for denial.
      * @return true if the api is denied for the given package-database pair.
      */
-    public boolean checkDeniedPackageDatabase(@NonNull String packageName,
-            @NonNull String databaseName, @CallStats.CallType int apiType) {
-        if (checkDeniedPackage(packageName, apiType) || checkDeniedDatabase(databaseName,
-                apiType)) {
+    public boolean checkDeniedPackageDatabase(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @CallStats.CallType int apiType) {
+        if (checkDeniedPackage(packageName, apiType)
+                || checkDeniedDatabase(databaseName, apiType)) {
             return true;
         }
         if (deniedPrefixes.isEmpty()) {
@@ -231,8 +240,8 @@
      * @param apiType the api type to check for denial.
      * @return true if the api is denied for the given package name.
      */
-    public boolean checkDeniedPackage(@NonNull String packageName,
-            @CallStats.CallType int apiType) {
+    public boolean checkDeniedPackage(
+            @NonNull String packageName, @CallStats.CallType int apiType) {
         Set<Integer> deniedApiTypes = deniedPackages.get(packageName);
         return deniedApiTypes != null && deniedApiTypes.contains(apiType);
     }
@@ -245,8 +254,8 @@
      * @param apiType the api type to check for denial.
      * @return true if the api is denied for the given database name.
      */
-    private boolean checkDeniedDatabase(@NonNull String databaseName,
-            @CallStats.CallType int apiType) {
+    private boolean checkDeniedDatabase(
+            @NonNull String databaseName, @CallStats.CallType int apiType) {
         Set<Integer> deniedApiTypes = deniedDatabases.get(databaseName);
         return deniedApiTypes != null && deniedApiTypes.contains(apiType);
     }
diff --git a/service/java/com/android/server/appsearch/FrameworkAppSearchConfigImpl.java b/service/java/com/android/server/appsearch/FrameworkAppSearchConfigImpl.java
deleted file mode 100644
index 7f82681..0000000
--- a/service/java/com/android/server/appsearch/FrameworkAppSearchConfigImpl.java
+++ /dev/null
@@ -1,828 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.android.server.appsearch;
-
-import android.annotation.NonNull;
-import android.os.Build;
-import android.os.Bundle;
-import android.provider.DeviceConfig;
-import android.provider.DeviceConfig.OnPropertiesChangedListener;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.appsearch.external.localstorage.IcingOptionsConfig;
-
-import java.util.Objects;
-import java.util.concurrent.Executor;
-
-/**
- * Implementation of {@link FrameworkAppSearchConfig} using {@link DeviceConfig}.
- *
- * <p>Though the latest flag values can always be retrieved by calling {@link
- * DeviceConfig#getProperty}, we want to cache some of those values. For example, the sampling
- * intervals for logging, they are needed for each api call and it would be a little expensive to
- * call {@link DeviceConfig#getProperty} every time.
- *
- * <p>Listener is registered to DeviceConfig keep the cached value up to date.
- *
- * <p>This class is thread-safe.
- *
- * @hide
- */
-public final class FrameworkAppSearchConfigImpl implements FrameworkAppSearchConfig {
-    private static volatile FrameworkAppSearchConfigImpl sConfig;
-
-    /*
-     * Keys for ALL the flags stored in DeviceConfig.
-     */
-    public static final String KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS =
-            "min_time_interval_between_samples_millis";
-    public static final String KEY_SAMPLING_INTERVAL_DEFAULT = "sampling_interval_default";
-    public static final String KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS =
-            "sampling_interval_for_batch_call_stats";
-    public static final String KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS =
-            "sampling_interval_for_put_document_stats";
-    public static final String KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS =
-            "sampling_interval_for_initialize_stats";
-    public static final String KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS =
-            "sampling_interval_for_search_stats";
-    public static final String KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS =
-            "sampling_interval_for_global_search_stats";
-    public static final String KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS =
-            "sampling_interval_for_optimize_stats";
-    public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES =
-            "limit_config_max_document_size_bytes";
-    public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT =
-            "limit_config_max_document_count";
-    public static final String KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT =
-            "limit_config_max_suggestion_count";
-    public static final String KEY_BYTES_OPTIMIZE_THRESHOLD = "bytes_optimize_threshold";
-    public static final String KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS = "time_optimize_threshold";
-    public static final String KEY_DOC_COUNT_OPTIMIZE_THRESHOLD = "doc_count_optimize_threshold";
-    public static final String KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS =
-            "min_time_optimize_threshold";
-    public static final String KEY_API_CALL_STATS_LIMIT = "api_call_stats_limit";
-    public static final String KEY_DENYLIST = "denylist";
-    public static final String KEY_RATE_LIMIT_ENABLED = "rate_limit_enabled";
-    public static final String KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY =
-            "rate_limit_task_queue_total_capacity";
-    public static final String KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE =
-            "rate_limit_task_queue_per_package_capacity_percentage";
-    public static final String KEY_RATE_LIMIT_API_COSTS = "rate_limit_api_costs";
-
-    public static final String KEY_ICING_MAX_TOKEN_LENGTH = "icing_max_token_length";
-    public static final String KEY_ICING_INDEX_MERGE_SIZE = "icing_index_merge_size";
-    public static final String KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT =
-            "icing_document_store_namespace_id_fingerprint";
-    public static final String KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD =
-            "icing_optimize_rebuild_index_threshold";
-    public static final String KEY_ICING_COMPRESSION_LEVEL = "icing_compression_level";
-    public static final String KEY_ICING_USE_READ_ONLY_SEARCH = "icing_use_read_only_search";
-    public static final String KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR =
-            "icing_use_pre_mapping_with_file_backed_vector";
-    public static final String KEY_ICING_USE_PERSISTENT_HASHMAP = "icing_use_persistent_hashmap";
-    public static final String KEY_ICING_MAX_PAGE_BYTES_LIMIT = "icing_max_page_bytes_limit";
-    public static final String KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD =
-            "icing_integer_index_bucket_split_threshold";
-    public static final String KEY_ICING_LITE_INDEX_SORT_AT_INDEXING =
-        "icing_lite_index_sort_at_indexing";
-    public static final String KEY_ICING_LITE_INDEX_SORT_SIZE =
-        "icing_lite_index_sort_size";
-    public static final String KEY_SHOULD_RETRIEVE_PARENT_INFO =
-        "should_retrieve_parent_info";
-    public static final String KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX =
-        "use_new_qualified_id_join_index";
-    public static final String KEY_BUILD_PROPERTY_EXISTENCE_METADATA_HITS =
-        "build_property_existence_metadata_hits";
-
-    /**
-     * This config does not need to be cached in FrameworkAppSearchConfigImpl as it is only accessed
-     * statically. AppSearch retrieves this directly from DeviceConfig when needed.
-     */
-    public static final String KEY_USE_FIXED_EXECUTOR_SERVICE = "use_fixed_executor_service";
-
-    // Array contains all the corresponding keys for the cached values.
-    private static final String[] KEYS_TO_ALL_CACHED_VALUES = {
-            KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
-            KEY_SAMPLING_INTERVAL_DEFAULT,
-            KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
-            KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
-            KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS,
-            KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS,
-            KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS,
-            KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS,
-            KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES,
-            KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
-            KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT,
-            KEY_BYTES_OPTIMIZE_THRESHOLD,
-            KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
-            KEY_DOC_COUNT_OPTIMIZE_THRESHOLD,
-            KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
-            KEY_API_CALL_STATS_LIMIT,
-            KEY_DENYLIST,
-            KEY_RATE_LIMIT_ENABLED,
-            KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
-            KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
-            KEY_RATE_LIMIT_API_COSTS,
-            KEY_ICING_MAX_TOKEN_LENGTH,
-            KEY_ICING_INDEX_MERGE_SIZE,
-            KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT,
-            KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD,
-            KEY_ICING_COMPRESSION_LEVEL,
-            KEY_ICING_USE_READ_ONLY_SEARCH,
-            KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR,
-            KEY_ICING_USE_PERSISTENT_HASHMAP,
-            KEY_ICING_MAX_PAGE_BYTES_LIMIT,
-            KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD,
-            KEY_ICING_LITE_INDEX_SORT_AT_INDEXING,
-            KEY_ICING_LITE_INDEX_SORT_SIZE,
-            KEY_SHOULD_RETRIEVE_PARENT_INFO,
-            KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX,
-            KEY_BUILD_PROPERTY_EXISTENCE_METADATA_HITS,
-    };
-
-    // Lock needed for all the operations in this class.
-    private final Object mLock = new Object();
-
-    /**
-     * Bundle to hold all the cached flag values corresponding to
-     * {@link FrameworkAppSearchConfigImpl#KEYS_TO_ALL_CACHED_VALUES}.
-     */
-    @GuardedBy("mLock")
-    private final Bundle mBundleLocked = new Bundle();
-
-    @GuardedBy("mLock")
-    private Denylist mDenylistLocked = Denylist.EMPTY_INSTANCE;
-
-    @GuardedBy("mLock")
-    private AppSearchRateLimitConfig mRateLimitConfigLocked = AppSearchRateLimitConfig.create(
-            DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
-            DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
-            DEFAULT_RATE_LIMIT_API_COSTS_STRING);
-
-    @GuardedBy("mLock")
-    private boolean mIsClosedLocked = false;
-
-    /** Listener to update cached flag values from DeviceConfig. */
-    private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
-            properties -> {
-                if (!properties.getNamespace().equals(DeviceConfig.NAMESPACE_APPSEARCH)) {
-                    return;
-                }
-
-                updateCachedValues(properties);
-            };
-
-    private FrameworkAppSearchConfigImpl() {
-    }
-
-    /**
-     * Creates an instance of {@link FrameworkAppSearchConfigImpl}.
-     *
-     * @param executor used to fetch and cache the flag values from DeviceConfig during creation or
-     *                 config change.
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @NonNull
-    public static FrameworkAppSearchConfigImpl create(@NonNull Executor executor) {
-        Objects.requireNonNull(executor);
-        FrameworkAppSearchConfigImpl configManager = new FrameworkAppSearchConfigImpl();
-        configManager.initialize(executor);
-        return configManager;
-    }
-
-    /**
-     * Gets an instance of {@link FrameworkAppSearchConfigImpl} to be used.
-     *
-     * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
-     * existing instance will be returned.
-     */
-    @NonNull
-    public static FrameworkAppSearchConfigImpl getInstance(@NonNull Executor executor) {
-        Objects.requireNonNull(executor);
-        if (sConfig == null) {
-            synchronized (FrameworkAppSearchConfigImpl.class) {
-                if (sConfig == null) {
-                    sConfig = create(executor);
-                }
-            }
-        }
-        return sConfig;
-    }
-
-    /**
-     * Returns whether or not to use a fixed executor service for AppSearch. This config is only
-     * queried statically and is therefore retrieved directly from DeviceConfig.
-     */
-    public static boolean getUseFixedExecutorService() {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_APPSEARCH,
-                KEY_USE_FIXED_EXECUTOR_SERVICE, DEFAULT_USE_FIXED_EXECUTOR_SERVICE);
-    }
-
-    /**
-     * Initializes the {@link FrameworkAppSearchConfigImpl}
-     *
-     * <p>It fetches the custom properties from DeviceConfig if available.
-     *
-     * @param executor listener would be run on to handle P/H flag change.
-     */
-    private void initialize(@NonNull Executor executor) {
-        executor.execute(() -> {
-            // Attach the callback to get updates on those properties.
-            DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APPSEARCH,
-                    executor,
-                    mOnDeviceConfigChangedListener);
-
-            DeviceConfig.Properties properties = DeviceConfig.getProperties(
-                    DeviceConfig.NAMESPACE_APPSEARCH, KEYS_TO_ALL_CACHED_VALUES);
-            updateCachedValues(properties);
-        });
-    }
-
-    // TODO(b/173532925) check this will be called. If we have a singleton instance for this
-    //  class, probably we don't need it.
-    @Override
-    public void close() {
-        synchronized (mLock) {
-            if (mIsClosedLocked) {
-                return;
-            }
-
-            DeviceConfig.removeOnPropertiesChangedListener(mOnDeviceConfigChangedListener);
-            mIsClosedLocked = true;
-        }
-    }
-
-    @Override
-    public long getCachedMinTimeIntervalBetweenSamplesMillis() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getLong(KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
-                    DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS);
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalDefault() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_DEFAULT, DEFAULT_SAMPLING_INTERVAL);
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalForBatchCallStats() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
-                    getCachedSamplingIntervalDefault());
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalForPutDocumentStats() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
-                    getCachedSamplingIntervalDefault());
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalForInitializeStats() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS,
-                    getCachedSamplingIntervalDefault());
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalForSearchStats() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS,
-                    getCachedSamplingIntervalDefault());
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalForGlobalSearchStats() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS,
-                    getCachedSamplingIntervalDefault());
-        }
-    }
-
-    @Override
-    public int getCachedSamplingIntervalForOptimizeStats() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS,
-                    getCachedSamplingIntervalDefault());
-        }
-    }
-
-    @Override
-    public int getMaxDocumentSizeBytes() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES,
-                    DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES);
-        }
-    }
-
-    @Override
-    public int getMaxDocumentCount() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
-                    DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT);
-        }
-    }
-
-    @Override
-    public int getMaxSuggestionCount() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT,
-                    DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT);
-        }
-    }
-
-    @Override
-    public int getCachedBytesOptimizeThreshold() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_BYTES_OPTIMIZE_THRESHOLD,
-                    DEFAULT_BYTES_OPTIMIZE_THRESHOLD);
-        }
-    }
-
-    @Override
-    public int getCachedTimeOptimizeThresholdMs() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
-                    DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS);
-        }
-    }
-
-    @Override
-    public int getCachedDocCountOptimizeThreshold() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_DOC_COUNT_OPTIMIZE_THRESHOLD,
-                    DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD);
-        }
-    }
-
-    @Override
-    public int getCachedMinTimeOptimizeThresholdMs() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
-                    DEFAULT_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS);
-        }
-    }
-
-    @Override
-    public int getCachedApiCallStatsLimit() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_API_CALL_STATS_LIMIT,
-                    DEFAULT_API_CALL_STATS_LIMIT);
-        }
-    }
-
-    @Override
-    public Denylist getCachedDenylist() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mDenylistLocked;
-        }
-    }
-
-    @Override
-    public int getMaxTokenLength() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_ICING_MAX_TOKEN_LENGTH,
-                    IcingOptionsConfig.DEFAULT_MAX_TOKEN_LENGTH);
-        }
-    }
-
-    @Override
-    public int getIndexMergeSize() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_ICING_INDEX_MERGE_SIZE,
-                    IcingOptionsConfig.DEFAULT_INDEX_MERGE_SIZE);
-        }
-    }
-
-    @Override
-    public boolean getDocumentStoreNamespaceIdFingerprint() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT,
-                    IcingOptionsConfig.DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT);
-        }
-    }
-
-    @Override
-    public float getOptimizeRebuildIndexThreshold() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getFloat(KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD,
-                    IcingOptionsConfig.DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD);
-        }
-    }
-
-    @Override
-    public int getCompressionLevel() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_ICING_COMPRESSION_LEVEL,
-                    IcingOptionsConfig.DEFAULT_COMPRESSION_LEVEL);
-        }
-    }
-
-    @Override
-    public boolean getAllowCircularSchemaDefinitions() {
-        // TODO(b/282108040) add flag(default on) to cover this feature in case a bug is discovered.
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
-        }
-    }
-
-    @Override
-    public boolean getUseReadOnlySearch() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(KEY_ICING_USE_READ_ONLY_SEARCH,
-                    DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH);
-        }
-    }
-
-    @Override
-    public boolean getUsePreMappingWithFileBackedVector() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR,
-                    IcingOptionsConfig.DEFAULT_USE_PREMAPPING_WITH_FILE_BACKED_VECTOR);
-        }
-    }
-
-    @Override
-    public boolean getUsePersistentHashMap() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(KEY_ICING_USE_PERSISTENT_HASHMAP,
-                    IcingOptionsConfig.DEFAULT_USE_PERSISTENT_HASH_MAP);
-        }
-    }
-
-    @Override
-    public int getMaxPageBytesLimit() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(KEY_ICING_MAX_PAGE_BYTES_LIMIT,
-                    IcingOptionsConfig.DEFAULT_MAX_PAGE_BYTES_LIMIT);
-        }
-    }
-
-    @Override
-    public boolean getCachedRateLimitEnabled() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(KEY_RATE_LIMIT_ENABLED, DEFAULT_RATE_LIMIT_ENABLED);
-        }
-    }
-
-    @Override
-    public AppSearchRateLimitConfig getCachedRateLimitConfig() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mRateLimitConfigLocked;
-        }
-    }
-
-    @Override
-    public int getIntegerIndexBucketSplitThreshold() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(
-                    KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD,
-                    DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD);
-        }
-    }
-
-    @Override
-    public boolean getLiteIndexSortAtIndexing() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(
-                KEY_ICING_LITE_INDEX_SORT_AT_INDEXING,
-                DEFAULT_LITE_INDEX_SORT_AT_INDEXING);
-        }
-    }
-
-    @Override
-    public int getLiteIndexSortSize() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getInt(
-                KEY_ICING_LITE_INDEX_SORT_SIZE,
-                DEFAULT_LITE_INDEX_SORT_SIZE);
-        }
-    }
-
-    @Override
-    public boolean getUseNewQualifiedIdJoinIndex() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(
-                KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX,
-                DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX);
-        }
-    }
-
-    @Override
-    public boolean getBuildPropertyExistenceMetadataHits() {
-        // This option is always true in Framework due to trunk stable frozen flags.
-        return true;
-    }
-
-    @Override
-    public boolean shouldStoreParentInfoAsSyntheticProperty() {
-      // This option is always true in Framework.
-      return true;
-    }
-
-    @Override
-    public boolean shouldRetrieveParentInfo() {
-        synchronized (mLock) {
-            throwIfClosedLocked();
-            return mBundleLocked.getBoolean(
-                KEY_SHOULD_RETRIEVE_PARENT_INFO,
-                DEFAULT_SHOULD_RETRIEVE_PARENT_INFO);
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void throwIfClosedLocked() {
-        if (mIsClosedLocked) {
-            throw new IllegalStateException("Trying to use a closed AppSearchConfig instance.");
-        }
-    }
-
-    private void updateCachedValues(@NonNull DeviceConfig.Properties properties) {
-        for (String key : properties.getKeyset()) {
-            updateCachedValue(key, properties);
-        }
-        updateDerivedClasses();
-    }
-
-    private void updateCachedValue(@NonNull String key,
-            @NonNull DeviceConfig.Properties properties) {
-        if (properties.getString(key, /*defaultValue=*/ null) == null) {
-            // Key is missing or value is just null. That is not expected if the key is
-            // defined in the configuration.
-            //
-            // We choose NOT to put the default value in the bundle.
-            // Instead, we let the getters handle what default value should be returned.
-            //
-            // Also we keep the old value in the bundle. So getters can still
-            // return last valid value.
-            return;
-        }
-
-        switch (key) {
-            case KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS:
-                synchronized (mLock) {
-                    mBundleLocked.putLong(key,
-                            properties.getLong(key,
-                                    DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS));
-                }
-                break;
-            case KEY_SAMPLING_INTERVAL_DEFAULT:
-            case KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS:
-            case KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS:
-            case KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS:
-            case KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS:
-            case KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS:
-            case KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key, DEFAULT_SAMPLING_INTERVAL));
-                }
-                break;
-            case KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(
-                            key,
-                            properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES));
-                }
-                break;
-            case KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(
-                            key,
-                            properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT));
-                }
-                break;
-            case KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(
-                            key,
-                            properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT));
-                }
-                break;
-            case KEY_BYTES_OPTIMIZE_THRESHOLD:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            DEFAULT_BYTES_OPTIMIZE_THRESHOLD));
-                }
-                break;
-            case KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS));
-                }
-                break;
-            case KEY_DOC_COUNT_OPTIMIZE_THRESHOLD:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD));
-                }
-                break;
-            case KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            DEFAULT_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS));
-                }
-                break;
-            case KEY_API_CALL_STATS_LIMIT:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key,
-                            properties.getInt(key, DEFAULT_API_CALL_STATS_LIMIT));
-                }
-                break;
-            case KEY_DENYLIST:
-                String denylistString = properties.getString(key, /* defaultValue= */ "");
-                Denylist denylist =
-                        denylistString.isEmpty() ? Denylist.EMPTY_INSTANCE : Denylist.create(
-                                denylistString);
-                synchronized (mLock) {
-                    mDenylistLocked = denylist;
-                }
-                break;
-            case KEY_RATE_LIMIT_ENABLED:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            DEFAULT_RATE_LIMIT_ENABLED));
-                }
-                break;
-            case KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY));
-                }
-                break;
-            case KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE:
-                synchronized (mLock) {
-                    mBundleLocked.putFloat(key, properties.getFloat(key,
-                            DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE));
-                }
-                break;
-            case KEY_RATE_LIMIT_API_COSTS:
-                synchronized (mLock) {
-                    mBundleLocked.putString(key, properties.getString(key,
-                            DEFAULT_RATE_LIMIT_API_COSTS_STRING));
-                }
-                break;
-            case KEY_ICING_MAX_TOKEN_LENGTH:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            IcingOptionsConfig.DEFAULT_MAX_TOKEN_LENGTH));
-                }
-                break;
-            case KEY_ICING_INDEX_MERGE_SIZE:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            IcingOptionsConfig.DEFAULT_INDEX_MERGE_SIZE));
-                }
-                break;
-            case KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            IcingOptionsConfig.DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT));
-                }
-                break;
-            case KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD:
-                synchronized (mLock) {
-                    mBundleLocked.putFloat(key, properties.getFloat(key,
-                            IcingOptionsConfig.DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD));
-                }
-                break;
-            case KEY_ICING_COMPRESSION_LEVEL:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            IcingOptionsConfig.DEFAULT_COMPRESSION_LEVEL));
-                }
-                break;
-            case KEY_ICING_USE_READ_ONLY_SEARCH:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH));
-                }
-                break;
-            case KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            IcingOptionsConfig.DEFAULT_USE_PREMAPPING_WITH_FILE_BACKED_VECTOR));
-                }
-                break;
-            case KEY_ICING_USE_PERSISTENT_HASHMAP:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            IcingOptionsConfig.DEFAULT_USE_PERSISTENT_HASH_MAP));
-                }
-                break;
-            case KEY_ICING_MAX_PAGE_BYTES_LIMIT:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            IcingOptionsConfig.DEFAULT_MAX_PAGE_BYTES_LIMIT));
-                }
-                break;
-            case KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            IcingOptionsConfig.DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD));
-                }
-                break;
-            case KEY_ICING_LITE_INDEX_SORT_AT_INDEXING:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            IcingOptionsConfig.DEFAULT_LITE_INDEX_SORT_AT_INDEXING));
-                }
-                break;
-            case KEY_ICING_LITE_INDEX_SORT_SIZE:
-                synchronized (mLock) {
-                    mBundleLocked.putInt(key, properties.getInt(key,
-                            IcingOptionsConfig.DEFAULT_LITE_INDEX_SORT_SIZE));
-                }
-                break;
-            case KEY_SHOULD_RETRIEVE_PARENT_INFO:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            DEFAULT_SHOULD_RETRIEVE_PARENT_INFO));
-                }
-                break;
-            case KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX:
-                synchronized (mLock) {
-                    mBundleLocked.putBoolean(key, properties.getBoolean(key,
-                            DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX));
-                }
-                break;
-            case KEY_BUILD_PROPERTY_EXISTENCE_METADATA_HITS:
-                // TODO(b/309826655) Set this value properly in main branch
-                // fall throw to default since we never turn this feature on in udc-mainline-prod
-            default:
-                break;
-        }
-    }
-
-    private void updateDerivedClasses() {
-        if (getCachedRateLimitEnabled()) {
-            synchronized (mLock) {
-                int taskQueueTotalCapacity = mBundleLocked.getInt(
-                        KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
-                        DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY);
-                float taskQueuePerPackagePercentage = mBundleLocked.getFloat(
-                        KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
-                        DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE);
-                String apiCostsString = mBundleLocked.getString(KEY_RATE_LIMIT_API_COSTS,
-                        DEFAULT_RATE_LIMIT_API_COSTS_STRING);
-                mRateLimitConfigLocked = mRateLimitConfigLocked.rebuildIfNecessary(
-                        taskQueueTotalCapacity, taskQueuePerPackagePercentage, apiCostsString);
-            }
-        }
-    }
-}
diff --git a/service/java/com/android/server/appsearch/FrameworkOptimizeStrategy.java b/service/java/com/android/server/appsearch/FrameworkOptimizeStrategy.java
deleted file mode 100644
index 7c40d94..0000000
--- a/service/java/com/android/server/appsearch/FrameworkOptimizeStrategy.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2021 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 com.android.server.appsearch;
-
-import android.annotation.NonNull;
-import android.util.Log;
-
-import com.android.server.appsearch.external.localstorage.AppSearchImpl;
-import com.android.server.appsearch.external.localstorage.OptimizeStrategy;
-
-import com.google.android.icing.proto.GetOptimizeInfoResultProto;
-
-import java.util.Objects;
-
-/**
- * An implementation of {@link OptimizeStrategy} will determine when to trigger {@link
- * AppSearchImpl#optimize()} in Jetpack environment.
- *
- * @hide
- */
-public class FrameworkOptimizeStrategy implements OptimizeStrategy {
-    private static final String TAG = "AppSearchOptimize";
-    private final FrameworkAppSearchConfig mAppSearchConfig;
-    FrameworkOptimizeStrategy(@NonNull FrameworkAppSearchConfig config) {
-        mAppSearchConfig = Objects.requireNonNull(config);
-    }
-
-    @Override
-    public boolean shouldOptimize(@NonNull GetOptimizeInfoResultProto optimizeInfo) {
-        boolean wantsOptimize =
-                optimizeInfo.getOptimizableDocs()
-                        >= mAppSearchConfig.getCachedDocCountOptimizeThreshold()
-                        || optimizeInfo.getEstimatedOptimizableBytes()
-                        >= mAppSearchConfig.getCachedBytesOptimizeThreshold()
-                        || optimizeInfo.getTimeSinceLastOptimizeMs()
-                        >= mAppSearchConfig.getCachedTimeOptimizeThresholdMs();
-        if (wantsOptimize &&
-                optimizeInfo.getTimeSinceLastOptimizeMs()
-                        < mAppSearchConfig.getCachedMinTimeOptimizeThresholdMs()) {
-            // TODO(b/271890504): Produce a log message for statsd when we skip a potential
-            //  compaction because the time since the last compaction has not reached
-            //  the minimum threshold.
-            Log.i(TAG, "Skipping optimization because time since last optimize ["
-                    + optimizeInfo.getTimeSinceLastOptimizeMs()
-                    + " ms] is lesser than the threshold for minimum time between optimizations ["
-                    + mAppSearchConfig.getCachedMinTimeOptimizeThresholdMs() + " ms]");
-            return false;
-        }
-        return wantsOptimize;
-    }
-}
diff --git a/service/java/com/android/server/appsearch/FrameworkServiceAppSearchConfig.java b/service/java/com/android/server/appsearch/FrameworkServiceAppSearchConfig.java
new file mode 100644
index 0000000..1467cad
--- /dev/null
+++ b/service/java/com/android/server/appsearch/FrameworkServiceAppSearchConfig.java
@@ -0,0 +1,910 @@
+/*
+ * Copyright (C) 2021 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 com.android.server.appsearch;
+
+import android.annotation.NonNull;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.OnPropertiesChangedListener;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.external.localstorage.IcingOptionsConfig;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of {@link ServiceAppSearchConfig} using {@link DeviceConfig}.
+ *
+ * <p>Though the latest flag values can always be retrieved by calling {@link
+ * DeviceConfig#getProperty}, we want to cache some of those values. For example, the sampling
+ * intervals for logging, they are needed for each api call and it would be a little expensive to
+ * call {@link DeviceConfig#getProperty} every time.
+ *
+ * <p>Listener is registered to DeviceConfig keep the cached value up to date.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @hide
+ */
+public final class FrameworkServiceAppSearchConfig implements ServiceAppSearchConfig {
+    private static volatile FrameworkServiceAppSearchConfig sConfig;
+
+    /*
+     * Keys for ALL the flags stored in DeviceConfig.
+     */
+    public static final String KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS =
+            "min_time_interval_between_samples_millis";
+    public static final String KEY_SAMPLING_INTERVAL_DEFAULT = "sampling_interval_default";
+    public static final String KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS =
+            "sampling_interval_for_batch_call_stats";
+    public static final String KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS =
+            "sampling_interval_for_put_document_stats";
+    public static final String KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS =
+            "sampling_interval_for_initialize_stats";
+    public static final String KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS =
+            "sampling_interval_for_search_stats";
+    public static final String KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS =
+            "sampling_interval_for_global_search_stats";
+    public static final String KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS =
+            "sampling_interval_for_optimize_stats";
+    public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES =
+            "limit_config_max_document_size_bytes";
+    public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT =
+            "limit_config_max_document_count";
+    public static final String KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT =
+            "limit_config_max_suggestion_count";
+    public static final String KEY_BYTES_OPTIMIZE_THRESHOLD = "bytes_optimize_threshold";
+    public static final String KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS = "time_optimize_threshold";
+    public static final String KEY_DOC_COUNT_OPTIMIZE_THRESHOLD = "doc_count_optimize_threshold";
+    public static final String KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS =
+            "min_time_optimize_threshold";
+    public static final String KEY_API_CALL_STATS_LIMIT = "api_call_stats_limit";
+    public static final String KEY_DENYLIST = "denylist";
+    public static final String KEY_RATE_LIMIT_ENABLED = "rate_limit_enabled";
+    public static final String KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY =
+            "rate_limit_task_queue_total_capacity";
+    public static final String KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE =
+            "rate_limit_task_queue_per_package_capacity_percentage";
+    public static final String KEY_RATE_LIMIT_API_COSTS = "rate_limit_api_costs";
+
+    public static final String KEY_ICING_MAX_TOKEN_LENGTH = "icing_max_token_length";
+    public static final String KEY_ICING_INDEX_MERGE_SIZE = "icing_index_merge_size";
+    public static final String KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT =
+            "icing_document_store_namespace_id_fingerprint";
+    public static final String KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD =
+            "icing_optimize_rebuild_index_threshold";
+    public static final String KEY_ICING_COMPRESSION_LEVEL = "icing_compression_level";
+    public static final String KEY_ICING_USE_READ_ONLY_SEARCH = "icing_use_read_only_search";
+    public static final String KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR =
+            "icing_use_pre_mapping_with_file_backed_vector";
+    public static final String KEY_ICING_USE_PERSISTENT_HASHMAP = "icing_use_persistent_hashmap";
+    public static final String KEY_ICING_MAX_PAGE_BYTES_LIMIT = "icing_max_page_bytes_limit";
+    public static final String KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD =
+            "icing_integer_index_bucket_split_threshold";
+    public static final String KEY_ICING_LITE_INDEX_SORT_AT_INDEXING =
+            "icing_lite_index_sort_at_indexing";
+    public static final String KEY_ICING_LITE_INDEX_SORT_SIZE = "icing_lite_index_sort_size";
+    public static final String KEY_SHOULD_RETRIEVE_PARENT_INFO = "should_retrieve_parent_info";
+    public static final String KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX =
+            "use_new_qualified_id_join_index";
+    public static final String KEY_BUILD_PROPERTY_EXISTENCE_METADATA_HITS =
+            "build_property_existence_metadata_hits";
+    public static final String KEY_APP_FUNCTION_CALL_TIMEOUT_MILLIS =
+            "app_function_call_timeout_millis";
+    public static final String KEY_FULLY_PERSIST_JOB_INTERVAL = "fully_persist_job_interval";
+
+    /**
+     * This config does not need to be cached in FrameworkServiceAppSearchConfig as it is only
+     * accessed statically. AppSearch retrieves this directly from DeviceConfig when needed.
+     */
+    public static final String KEY_USE_FIXED_EXECUTOR_SERVICE = "use_fixed_executor_service";
+
+    // Array contains all the corresponding keys for the cached values.
+    private static final String[] KEYS_TO_ALL_CACHED_VALUES = {
+        KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
+        KEY_SAMPLING_INTERVAL_DEFAULT,
+        KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
+        KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
+        KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS,
+        KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS,
+        KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS,
+        KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS,
+        KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES,
+        KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
+        KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT,
+        KEY_BYTES_OPTIMIZE_THRESHOLD,
+        KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
+        KEY_DOC_COUNT_OPTIMIZE_THRESHOLD,
+        KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
+        KEY_API_CALL_STATS_LIMIT,
+        KEY_DENYLIST,
+        KEY_RATE_LIMIT_ENABLED,
+        KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
+        KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
+        KEY_RATE_LIMIT_API_COSTS,
+        KEY_ICING_MAX_TOKEN_LENGTH,
+        KEY_ICING_INDEX_MERGE_SIZE,
+        KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT,
+        KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD,
+        KEY_ICING_COMPRESSION_LEVEL,
+        KEY_ICING_USE_READ_ONLY_SEARCH,
+        KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR,
+        KEY_ICING_USE_PERSISTENT_HASHMAP,
+        KEY_ICING_MAX_PAGE_BYTES_LIMIT,
+        KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD,
+        KEY_ICING_LITE_INDEX_SORT_AT_INDEXING,
+        KEY_ICING_LITE_INDEX_SORT_SIZE,
+        KEY_SHOULD_RETRIEVE_PARENT_INFO,
+        KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX,
+        KEY_BUILD_PROPERTY_EXISTENCE_METADATA_HITS,
+        KEY_APP_FUNCTION_CALL_TIMEOUT_MILLIS,
+        KEY_FULLY_PERSIST_JOB_INTERVAL
+    };
+
+    // Lock needed for all the operations in this class.
+    private final Object mLock = new Object();
+
+    /**
+     * Bundle to hold all the cached flag values corresponding to {@link
+     * FrameworkServiceAppSearchConfig#KEYS_TO_ALL_CACHED_VALUES}.
+     */
+    @GuardedBy("mLock")
+    private final Bundle mBundleLocked = new Bundle();
+
+    @GuardedBy("mLock")
+    private Denylist mDenylistLocked = Denylist.EMPTY_INSTANCE;
+
+    @GuardedBy("mLock")
+    private AppSearchRateLimitConfig mRateLimitConfigLocked =
+            AppSearchRateLimitConfig.create(
+                    DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
+                    DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
+                    DEFAULT_RATE_LIMIT_API_COSTS_STRING);
+
+    @GuardedBy("mLock")
+    private boolean mIsClosedLocked = false;
+
+    /** Listener to update cached flag values from DeviceConfig. */
+    private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
+            properties -> {
+                if (!properties.getNamespace().equals(DeviceConfig.NAMESPACE_APPSEARCH)) {
+                    return;
+                }
+
+                updateCachedValues(properties);
+            };
+
+    private FrameworkServiceAppSearchConfig() {}
+
+    /**
+     * Creates an instance of {@link FrameworkServiceAppSearchConfig}.
+     *
+     * @param executor used to fetch and cache the flag values from DeviceConfig during creation or
+     *     config change.
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    @NonNull
+    public static FrameworkServiceAppSearchConfig create(@NonNull Executor executor) {
+        Objects.requireNonNull(executor);
+        FrameworkServiceAppSearchConfig configManager = new FrameworkServiceAppSearchConfig();
+        configManager.initialize(executor);
+        return configManager;
+    }
+
+    /**
+     * Gets an instance of {@link FrameworkServiceAppSearchConfig} to be used.
+     *
+     * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
+     * existing instance will be returned.
+     */
+    @NonNull
+    public static FrameworkServiceAppSearchConfig getInstance(@NonNull Executor executor) {
+        Objects.requireNonNull(executor);
+        if (sConfig == null) {
+            synchronized (FrameworkServiceAppSearchConfig.class) {
+                if (sConfig == null) {
+                    sConfig = create(executor);
+                }
+            }
+        }
+        return sConfig;
+    }
+
+    /**
+     * Returns whether or not to use a fixed executor service for AppSearch. This config is only
+     * queried statically and is therefore retrieved directly from DeviceConfig.
+     */
+    public static boolean getUseFixedExecutorService() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_USE_FIXED_EXECUTOR_SERVICE,
+                DEFAULT_USE_FIXED_EXECUTOR_SERVICE);
+    }
+
+    /**
+     * Initializes the {@link FrameworkServiceAppSearchConfig}
+     *
+     * <p>It fetches the custom properties from DeviceConfig if available.
+     *
+     * @param executor listener would be run on to handle P/H flag change.
+     */
+    private void initialize(@NonNull Executor executor) {
+        executor.execute(
+                () -> {
+                    // Attach the callback to get updates on those properties.
+                    DeviceConfig.addOnPropertiesChangedListener(
+                            DeviceConfig.NAMESPACE_APPSEARCH,
+                            executor,
+                            mOnDeviceConfigChangedListener);
+
+                    DeviceConfig.Properties properties =
+                            DeviceConfig.getProperties(
+                                    DeviceConfig.NAMESPACE_APPSEARCH, KEYS_TO_ALL_CACHED_VALUES);
+                    updateCachedValues(properties);
+                });
+    }
+
+    // TODO(b/173532925) check this will be called. If we have a singleton instance for this
+    //  class, probably we don't need it.
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            if (mIsClosedLocked) {
+                return;
+            }
+
+            DeviceConfig.removeOnPropertiesChangedListener(mOnDeviceConfigChangedListener);
+            mIsClosedLocked = true;
+        }
+    }
+
+    @Override
+    public long getCachedMinTimeIntervalBetweenSamplesMillis() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getLong(
+                    KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
+                    DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS);
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalDefault() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_DEFAULT, DEFAULT_SAMPLING_INTERVAL);
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalForBatchCallStats() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS, getCachedSamplingIntervalDefault());
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalForPutDocumentStats() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
+                    getCachedSamplingIntervalDefault());
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalForInitializeStats() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS, getCachedSamplingIntervalDefault());
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalForSearchStats() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS, getCachedSamplingIntervalDefault());
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalForGlobalSearchStats() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS,
+                    getCachedSamplingIntervalDefault());
+        }
+    }
+
+    @Override
+    public int getCachedSamplingIntervalForOptimizeStats() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS, getCachedSamplingIntervalDefault());
+        }
+    }
+
+    @Override
+    public int getMaxDocumentSizeBytes() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES,
+                    DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES);
+        }
+    }
+
+    @Override
+    public int getMaxDocumentCount() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT);
+        }
+    }
+
+    @Override
+    public int getMaxSuggestionCount() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT,
+                    DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT);
+        }
+    }
+
+    @Override
+    public int getCachedBytesOptimizeThreshold() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_BYTES_OPTIMIZE_THRESHOLD, DEFAULT_BYTES_OPTIMIZE_THRESHOLD);
+        }
+    }
+
+    @Override
+    public int getCachedTimeOptimizeThresholdMs() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS, DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS);
+        }
+    }
+
+    @Override
+    public int getCachedDocCountOptimizeThreshold() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_DOC_COUNT_OPTIMIZE_THRESHOLD, DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD);
+        }
+    }
+
+    @Override
+    public int getCachedMinTimeOptimizeThresholdMs() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
+                    DEFAULT_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS);
+        }
+    }
+
+    @Override
+    public int getCachedApiCallStatsLimit() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(KEY_API_CALL_STATS_LIMIT, DEFAULT_API_CALL_STATS_LIMIT);
+        }
+    }
+
+    @Override
+    public Denylist getCachedDenylist() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mDenylistLocked;
+        }
+    }
+
+    @Override
+    public int getMaxTokenLength() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_ICING_MAX_TOKEN_LENGTH, IcingOptionsConfig.DEFAULT_MAX_TOKEN_LENGTH);
+        }
+    }
+
+    @Override
+    public int getIndexMergeSize() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_ICING_INDEX_MERGE_SIZE, IcingOptionsConfig.DEFAULT_INDEX_MERGE_SIZE);
+        }
+    }
+
+    @Override
+    public boolean getDocumentStoreNamespaceIdFingerprint() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT,
+                    IcingOptionsConfig.DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT);
+        }
+    }
+
+    @Override
+    public float getOptimizeRebuildIndexThreshold() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getFloat(
+                    KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD,
+                    IcingOptionsConfig.DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD);
+        }
+    }
+
+    @Override
+    public int getCompressionLevel() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_ICING_COMPRESSION_LEVEL, IcingOptionsConfig.DEFAULT_COMPRESSION_LEVEL);
+        }
+    }
+
+    @Override
+    public boolean getAllowCircularSchemaDefinitions() {
+        // TODO(b/282108040) add flag(default on) to cover this feature in case a bug is discovered.
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+        }
+    }
+
+    @Override
+    public boolean getUseReadOnlySearch() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_ICING_USE_READ_ONLY_SEARCH, DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH);
+        }
+    }
+
+    @Override
+    public boolean getUsePreMappingWithFileBackedVector() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR,
+                    IcingOptionsConfig.DEFAULT_USE_PREMAPPING_WITH_FILE_BACKED_VECTOR);
+        }
+    }
+
+    @Override
+    public boolean getUsePersistentHashMap() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_ICING_USE_PERSISTENT_HASHMAP,
+                    IcingOptionsConfig.DEFAULT_USE_PERSISTENT_HASH_MAP);
+        }
+    }
+
+    @Override
+    public int getMaxPageBytesLimit() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_ICING_MAX_PAGE_BYTES_LIMIT,
+                    IcingOptionsConfig.DEFAULT_MAX_PAGE_BYTES_LIMIT);
+        }
+    }
+
+    @Override
+    public boolean getCachedRateLimitEnabled() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(KEY_RATE_LIMIT_ENABLED, DEFAULT_RATE_LIMIT_ENABLED);
+        }
+    }
+
+    @Override
+    public AppSearchRateLimitConfig getCachedRateLimitConfig() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mRateLimitConfigLocked;
+        }
+    }
+
+    @Override
+    public long getAppFunctionCallTimeoutMillis() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getLong(
+                    KEY_APP_FUNCTION_CALL_TIMEOUT_MILLIS, DEFAULT_APP_FUNCTION_CALL_TIMEOUT_MILLIS);
+        }
+    }
+
+    @Override
+    public long getCachedFullyPersistJobIntervalMillis() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getLong(
+                    KEY_FULLY_PERSIST_JOB_INTERVAL, DEFAULT_FULLY_PERSIST_JOB_INTERVAL);
+        }
+    }
+
+    @Override
+    public int getIntegerIndexBucketSplitThreshold() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD,
+                    DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD);
+        }
+    }
+
+    @Override
+    public boolean getLiteIndexSortAtIndexing() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_ICING_LITE_INDEX_SORT_AT_INDEXING, DEFAULT_LITE_INDEX_SORT_AT_INDEXING);
+        }
+    }
+
+    @Override
+    public int getLiteIndexSortSize() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getInt(
+                    KEY_ICING_LITE_INDEX_SORT_SIZE, DEFAULT_LITE_INDEX_SORT_SIZE);
+        }
+    }
+
+    @Override
+    public boolean getUseNewQualifiedIdJoinIndex() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX, DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX);
+        }
+    }
+
+    @Override
+    public boolean getBuildPropertyExistenceMetadataHits() {
+        // This option is always true in Framework due to trunk stable frozen flags.
+        return true;
+    }
+
+    @Override
+    public boolean shouldStoreParentInfoAsSyntheticProperty() {
+        // This option is always true in Framework.
+        return true;
+    }
+
+    @Override
+    public boolean shouldRetrieveParentInfo() {
+        synchronized (mLock) {
+            throwIfClosedLocked();
+            return mBundleLocked.getBoolean(
+                    KEY_SHOULD_RETRIEVE_PARENT_INFO, DEFAULT_SHOULD_RETRIEVE_PARENT_INFO);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void throwIfClosedLocked() {
+        if (mIsClosedLocked) {
+            throw new IllegalStateException("Trying to use a closed AppSearchConfig instance.");
+        }
+    }
+
+    private void updateCachedValues(@NonNull DeviceConfig.Properties properties) {
+        for (String key : properties.getKeyset()) {
+            updateCachedValue(key, properties);
+        }
+        updateDerivedClasses();
+    }
+
+    private void updateCachedValue(
+            @NonNull String key, @NonNull DeviceConfig.Properties properties) {
+        if (properties.getString(key, /* defaultValue= */ null) == null) {
+            // Key is missing or value is just null. That is not expected if the key is
+            // defined in the configuration.
+            //
+            // We choose NOT to put the default value in the bundle.
+            // Instead, we let the getters handle what default value should be returned.
+            //
+            // Also we keep the old value in the bundle. So getters can still
+            // return last valid value.
+            return;
+        }
+
+        switch (key) {
+            case KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS:
+                synchronized (mLock) {
+                    mBundleLocked.putLong(
+                            key,
+                            properties.getLong(
+                                    key, DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS));
+                }
+                break;
+            case KEY_SAMPLING_INTERVAL_DEFAULT:
+            case KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS:
+            case KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS:
+            case KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS:
+            case KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS:
+            case KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS:
+            case KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(key, properties.getInt(key, DEFAULT_SAMPLING_INTERVAL));
+                }
+                break;
+            case KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES));
+                }
+                break;
+            case KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key, properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT));
+                }
+                break;
+            case KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key, properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT));
+                }
+                break;
+            case KEY_BYTES_OPTIMIZE_THRESHOLD:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key, properties.getInt(key, DEFAULT_BYTES_OPTIMIZE_THRESHOLD));
+                }
+                break;
+            case KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key, properties.getInt(key, DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS));
+                }
+                break;
+            case KEY_DOC_COUNT_OPTIMIZE_THRESHOLD:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key, properties.getInt(key, DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD));
+                }
+                break;
+            case KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(key, DEFAULT_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS));
+                }
+                break;
+            case KEY_API_CALL_STATS_LIMIT:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(key, properties.getInt(key, DEFAULT_API_CALL_STATS_LIMIT));
+                }
+                break;
+            case KEY_DENYLIST:
+                String denylistString = properties.getString(key, /* defaultValue= */ "");
+                Denylist denylist =
+                        denylistString.isEmpty()
+                                ? Denylist.EMPTY_INSTANCE
+                                : Denylist.create(denylistString);
+                synchronized (mLock) {
+                    mDenylistLocked = denylist;
+                }
+                break;
+            case KEY_RATE_LIMIT_ENABLED:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key, properties.getBoolean(key, DEFAULT_RATE_LIMIT_ENABLED));
+                }
+                break;
+            case KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(key, DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY));
+                }
+                break;
+            case KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE:
+                synchronized (mLock) {
+                    mBundleLocked.putFloat(
+                            key,
+                            properties.getFloat(
+                                    key,
+                                    DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE));
+                }
+                break;
+            case KEY_RATE_LIMIT_API_COSTS:
+                synchronized (mLock) {
+                    mBundleLocked.putString(
+                            key, properties.getString(key, DEFAULT_RATE_LIMIT_API_COSTS_STRING));
+                }
+                break;
+            case KEY_ICING_MAX_TOKEN_LENGTH:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(key, IcingOptionsConfig.DEFAULT_MAX_TOKEN_LENGTH));
+                }
+                break;
+            case KEY_ICING_INDEX_MERGE_SIZE:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(key, IcingOptionsConfig.DEFAULT_INDEX_MERGE_SIZE));
+                }
+                break;
+            case KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key,
+                            properties.getBoolean(
+                                    key,
+                                    IcingOptionsConfig
+                                            .DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT));
+                }
+                break;
+            case KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD:
+                synchronized (mLock) {
+                    mBundleLocked.putFloat(
+                            key,
+                            properties.getFloat(
+                                    key,
+                                    IcingOptionsConfig.DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD));
+                }
+                break;
+            case KEY_ICING_COMPRESSION_LEVEL:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(key, IcingOptionsConfig.DEFAULT_COMPRESSION_LEVEL));
+                }
+                break;
+            case KEY_ICING_USE_READ_ONLY_SEARCH:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key,
+                            properties.getBoolean(key, DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH));
+                }
+                break;
+            case KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key,
+                            properties.getBoolean(
+                                    key,
+                                    IcingOptionsConfig
+                                            .DEFAULT_USE_PREMAPPING_WITH_FILE_BACKED_VECTOR));
+                }
+                break;
+            case KEY_ICING_USE_PERSISTENT_HASHMAP:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key,
+                            properties.getBoolean(
+                                    key, IcingOptionsConfig.DEFAULT_USE_PERSISTENT_HASH_MAP));
+                }
+                break;
+            case KEY_ICING_MAX_PAGE_BYTES_LIMIT:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(
+                                    key, IcingOptionsConfig.DEFAULT_MAX_PAGE_BYTES_LIMIT));
+                }
+                break;
+            case KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(
+                                    key,
+                                    IcingOptionsConfig
+                                            .DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD));
+                }
+                break;
+            case KEY_ICING_LITE_INDEX_SORT_AT_INDEXING:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key,
+                            properties.getBoolean(
+                                    key, IcingOptionsConfig.DEFAULT_LITE_INDEX_SORT_AT_INDEXING));
+                }
+                break;
+            case KEY_ICING_LITE_INDEX_SORT_SIZE:
+                synchronized (mLock) {
+                    mBundleLocked.putInt(
+                            key,
+                            properties.getInt(
+                                    key, IcingOptionsConfig.DEFAULT_LITE_INDEX_SORT_SIZE));
+                }
+                break;
+            case KEY_SHOULD_RETRIEVE_PARENT_INFO:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key, properties.getBoolean(key, DEFAULT_SHOULD_RETRIEVE_PARENT_INFO));
+                }
+                break;
+            case KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX:
+                synchronized (mLock) {
+                    mBundleLocked.putBoolean(
+                            key,
+                            properties.getBoolean(key, DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX));
+                }
+                break;
+            case KEY_APP_FUNCTION_CALL_TIMEOUT_MILLIS:
+                synchronized (mLock) {
+                    mBundleLocked.putLong(
+                            key, properties.getLong(key, DEFAULT_APP_FUNCTION_CALL_TIMEOUT_MILLIS));
+                }
+                break;
+            case KEY_FULLY_PERSIST_JOB_INTERVAL:
+                synchronized (mLock) {
+                    mBundleLocked.putLong(
+                            key, properties.getLong(key, DEFAULT_FULLY_PERSIST_JOB_INTERVAL));
+                }
+                break;
+            case KEY_BUILD_PROPERTY_EXISTENCE_METADATA_HITS:
+                // TODO(b/309826655) Set this value properly in main branch
+                // fall throw to default since we never turn this feature on in udc-mainline-prod
+            default:
+                break;
+        }
+    }
+
+    private void updateDerivedClasses() {
+        if (getCachedRateLimitEnabled()) {
+            synchronized (mLock) {
+                int taskQueueTotalCapacity =
+                        mBundleLocked.getInt(
+                                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY,
+                                DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY);
+                float taskQueuePerPackagePercentage =
+                        mBundleLocked.getFloat(
+                                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
+                                DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE);
+                String apiCostsString =
+                        mBundleLocked.getString(
+                                KEY_RATE_LIMIT_API_COSTS, DEFAULT_RATE_LIMIT_API_COSTS_STRING);
+                mRateLimitConfigLocked =
+                        mRateLimitConfigLocked.rebuildIfNecessary(
+                                taskQueueTotalCapacity,
+                                taskQueuePerPackagePercentage,
+                                apiCostsString);
+            }
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/InternalAppSearchLogger.java b/service/java/com/android/server/appsearch/InternalAppSearchLogger.java
index c1f5e73..1cbeecb 100644
--- a/service/java/com/android/server/appsearch/InternalAppSearchLogger.java
+++ b/service/java/com/android/server/appsearch/InternalAppSearchLogger.java
@@ -25,6 +25,7 @@
 
 /**
  * A non-public interface for implementing AppSearch logging based operations stats.
+ *
  * @hide
  */
 public interface InternalAppSearchLogger extends AppSearchLogger {
diff --git a/service/java/com/android/server/appsearch/FrameworkAppSearchConfig.java b/service/java/com/android/server/appsearch/ServiceAppSearchConfig.java
similarity index 67%
rename from service/java/com/android/server/appsearch/FrameworkAppSearchConfig.java
rename to service/java/com/android/server/appsearch/ServiceAppSearchConfig.java
index cc35d5e..ab28f01 100644
--- a/service/java/com/android/server/appsearch/FrameworkAppSearchConfig.java
+++ b/service/java/com/android/server/appsearch/ServiceAppSearchConfig.java
@@ -16,56 +16,61 @@
 
 package com.android.server.appsearch;
 
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+
 import com.android.server.appsearch.external.localstorage.AppSearchConfig;
 
 /**
  * An interface which exposes config flags to AppSearch.
  *
- * <p>This interface provides an abstraction for the platform's flag mechanism and implements
- * caching to avoid expensive lookups.
+ * <p>This interface provides an abstraction for the AppSearch's flag mechanism and implements
+ * caching to avoid expensive lookups. This interface is only used by environments which have a
+ * running AppSearch service like Framework and GMSCore. JetPack uses {@link AppSearchConfig}
+ * directly instead.
  *
  * <p>Implementations of this interface must be thread-safe.
  *
  * @hide
  */
-public interface FrameworkAppSearchConfig extends AppSearchConfig, AutoCloseable {
+public interface ServiceAppSearchConfig extends AppSearchConfig, AutoCloseable {
     /**
-     * Default min time interval between samples in millis if there is no value set for
-     * {@link #getCachedMinTimeIntervalBetweenSamplesMillis()} in the flag system.
+     * Default min time interval between samples in millis if there is no value set for {@link
+     * #getCachedMinTimeIntervalBetweenSamplesMillis()} in the flag system.
      */
     long DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS = 50;
 
     /**
-     * Default sampling interval if there is no value set for
-     * {@link #getCachedSamplingIntervalDefault()} in the flag system.
+     * Default sampling interval if there is no value set for {@link
+     * #getCachedSamplingIntervalDefault()} in the flag system.
      */
     int DEFAULT_SAMPLING_INTERVAL = 10;
 
     int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES = 512 * 1024; // 512KiB
-    int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT = 20_000;
+    int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT = 80_000;
     int DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT = 20_000;
-    int DEFAULT_BYTES_OPTIMIZE_THRESHOLD = 1 * 1024 * 1024; // 1 MiB
-    int DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS = Integer.MAX_VALUE;
+    int DEFAULT_BYTES_OPTIMIZE_THRESHOLD = 10 * 1024 * 1024; // 10 MiB
+    int DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS = 7 * 24 * 60 * 60 * 1000; // 7 days in millis
     int DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD = 10_000;
     int DEFAULT_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS = 0;
     // Cached API Call Stats is disabled by default
     int DEFAULT_API_CALL_STATS_LIMIT = 0;
     boolean DEFAULT_RATE_LIMIT_ENABLED = false;
-    /**
-     * This defines the task queue's total capacity for rate limiting.
-     */
+
+    /** This defines the task queue's total capacity for rate limiting. */
     int DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY = Integer.MAX_VALUE;
+
     /**
      * This defines the per-package capacity for rate limiting as a percentage of the total
      * capacity.
      */
     float DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE = 1;
+
     /**
      * This defines API costs used for AppSearch's task queue rate limit.
      *
      * <p>Each entry in the string should follow the format 'api_name:integer_cost', and each entry
-     * should be separated by a semi-colon. API names should follow the string definitions in
-     * {@link com.android.server.appsearch.external.localstorage.stats.CallStats}.
+     * should be separated by a semi-colon. API names should follow the string definitions in {@link
+     * com.android.server.appsearch.external.localstorage.stats.CallStats}.
      *
      * <p>e.g. A valid string: "localPutDocuments:5;localSearch:1;localSetSchema:10"
      */
@@ -73,19 +78,20 @@
 
     boolean DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH = true;
     boolean DEFAULT_USE_FIXED_EXECUTOR_SERVICE = false;
+    long DEFAULT_APP_FUNCTION_CALL_TIMEOUT_MILLIS = 30_000;
 
-
-    /**
-     * This flag value is true by default because the flag is intended as a kill-switch.
-     */
+    /** This flag value is true by default because the flag is intended as a kill-switch. */
     boolean DEFAULT_SHOULD_RETRIEVE_PARENT_INFO = true;
 
+    /** The default interval in millisecond to trigger fully persist job. */
+    long DEFAULT_FULLY_PERSIST_JOB_INTERVAL = DAY_IN_MILLIS;
+
     /** Returns cached value for minTimeIntervalBetweenSamplesMillis. */
     long getCachedMinTimeIntervalBetweenSamplesMillis();
 
     /**
-     * Returns cached value for default sampling interval for all the stats NOT listed in
-     * the configuration.
+     * Returns cached value for default sampling interval for all the stats NOT listed in the
+     * configuration.
      *
      * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
      */
@@ -136,7 +142,7 @@
     /**
      * Returns the cached optimize byte size threshold.
      *
-     * An AppSearch Optimize job will be triggered if the bytes size of garbage resource exceeds
+     * <p>An AppSearch Optimize job will be triggered if the bytes size of garbage resource exceeds
      * this threshold.
      */
     int getCachedBytesOptimizeThreshold();
@@ -144,7 +150,7 @@
     /**
      * Returns the cached optimize time interval threshold.
      *
-     * An AppSearch Optimize job will be triggered if the time since last optimize job exceeds
+     * <p>An AppSearch Optimize job will be triggered if the time since last optimize job exceeds
      * this threshold.
      */
     int getCachedTimeOptimizeThresholdMs();
@@ -152,7 +158,7 @@
     /**
      * Returns the cached optimize document count threshold.
      *
-     * An AppSearch Optimize job will be triggered if the number of document of garbage resource
+     * <p>An AppSearch Optimize job will be triggered if the number of document of garbage resource
      * exceeds this threshold.
      */
     int getCachedDocCountOptimizeThreshold();
@@ -160,32 +166,36 @@
     /**
      * Returns the cached minimum optimize time interval threshold.
      *
-     * An AppSearch Optimize job will only be triggered if the time since last optimize job exceeds
-     * this threshold.
+     * <p>An AppSearch Optimize job will only be triggered if the time since last optimize job
+     * exceeds this threshold.
      */
     int getCachedMinTimeOptimizeThresholdMs();
 
-    /**
-     * Returns the maximum number of last API calls' statistics that can be included in dumpsys.
-     */
+    /** Returns the maximum number of last API calls' statistics that can be included in dumpsys. */
     int getCachedApiCallStatsLimit();
 
-    /**
-     * Returns the cached denylist.
-     */
+    /** Returns the cached denylist. */
     Denylist getCachedDenylist();
 
-    /**
-     * Returns whether to enable AppSearch rate limiting.
-     */
+    /** Returns whether to enable AppSearch rate limiting. */
     boolean getCachedRateLimitEnabled();
 
-    /**
-     * Returns the cached {@link AppSearchRateLimitConfig}.
-     */
+    /** Returns the cached {@link AppSearchRateLimitConfig}. */
     AppSearchRateLimitConfig getCachedRateLimitConfig();
 
     /**
+     * Returns the maximum allowed duration for an app function call in milliseconds.
+     *
+     * @see android.app.appsearch.functions.AppFunctionManager#executeAppFunction
+     */
+    long getAppFunctionCallTimeoutMillis();
+
+    /**
+     * Returns the time interval to schedule a full persist to disk back ground job in milliseconds.
+     */
+    long getCachedFullyPersistJobIntervalMillis();
+
+    /**
      * Closes this {@link AppSearchConfig}.
      *
      * <p>This close() operation does not throw an exception.
diff --git a/service/java/com/android/server/appsearch/ServiceOptimizeStrategy.java b/service/java/com/android/server/appsearch/ServiceOptimizeStrategy.java
new file mode 100644
index 0000000..c3f5a76
--- /dev/null
+++ b/service/java/com/android/server/appsearch/ServiceOptimizeStrategy.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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 com.android.server.appsearch;
+
+import android.annotation.NonNull;
+import android.app.appsearch.util.LogUtil;
+import android.util.Log;
+
+import com.android.server.appsearch.external.localstorage.AppSearchImpl;
+import com.android.server.appsearch.external.localstorage.OptimizeStrategy;
+
+import com.google.android.icing.proto.GetOptimizeInfoResultProto;
+
+import java.util.Objects;
+
+/**
+ * An implementation of {@link OptimizeStrategy} will determine when to trigger {@link
+ * AppSearchImpl#optimize()} based on last time optimize ran and number of bytes to optimize. This
+ * implementation is used by environments with AppSearch service running like Framework and GMSCore.
+ *
+ * @hide
+ */
+public class ServiceOptimizeStrategy implements OptimizeStrategy {
+    private static final String TAG = "AppSearchOptimize";
+    private final ServiceAppSearchConfig mAppSearchConfig;
+
+    ServiceOptimizeStrategy(@NonNull ServiceAppSearchConfig config) {
+        mAppSearchConfig = Objects.requireNonNull(config);
+    }
+
+    @Override
+    public boolean shouldOptimize(@NonNull GetOptimizeInfoResultProto optimizeInfo) {
+        boolean wantsOptimize =
+                optimizeInfo.getOptimizableDocs()
+                                >= mAppSearchConfig.getCachedDocCountOptimizeThreshold()
+                        || optimizeInfo.getEstimatedOptimizableBytes()
+                                >= mAppSearchConfig.getCachedBytesOptimizeThreshold()
+                        || optimizeInfo.getTimeSinceLastOptimizeMs()
+                                >= mAppSearchConfig.getCachedTimeOptimizeThresholdMs();
+        if (wantsOptimize
+                && optimizeInfo.getTimeSinceLastOptimizeMs()
+                        < mAppSearchConfig.getCachedMinTimeOptimizeThresholdMs()) {
+            // TODO(b/271890504): Produce a log message for statsd when we skip a potential
+            //  compaction because the time since the last compaction has not reached
+            //  the minimum threshold.
+            if (LogUtil.INFO) {
+                Log.i(
+                        TAG,
+                        "Skipping optimization because time since last optimize ["
+                                + optimizeInfo.getTimeSinceLastOptimizeMs()
+                                + " ms] is lesser than the threshold for minimum time between"
+                                + " optimizations ["
+                                + mAppSearchConfig.getCachedMinTimeOptimizeThresholdMs()
+                                + " ms]");
+            }
+            return false;
+        }
+        return wantsOptimize;
+    }
+}
diff --git a/service/java/com/android/server/appsearch/UserStorageInfo.java b/service/java/com/android/server/appsearch/UserStorageInfo.java
index 089c818..89f63eb 100644
--- a/service/java/com/android/server/appsearch/UserStorageInfo.java
+++ b/service/java/com/android/server/appsearch/UserStorageInfo.java
@@ -19,13 +19,16 @@
 import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.getPackageName;
 
 import android.annotation.NonNull;
+import android.app.appsearch.checker.initialization.qual.UnderInitialization;
+import android.app.appsearch.checker.initialization.qual.UnknownInitialization;
+import android.app.appsearch.checker.nullness.qual.RequiresNonNull;
 import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.util.ExceptionUtil;
 import android.util.ArrayMap;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.appsearch.external.localstorage.AppSearchImpl;
-import com.android.server.appsearch.util.ExceptionUtil;
 
 import com.google.android.icing.proto.DocumentStorageInfoProto;
 import com.google.android.icing.proto.NamespaceStorageInfoProto;
@@ -62,8 +65,7 @@
     }
 
     /**
-     * Updates storage info file with the latest storage info queried through
-     * {@link AppSearchImpl}.
+     * Updates storage info file with the latest storage info queried through {@link AppSearchImpl}.
      */
     public void updateStorageInfoFile(@NonNull AppSearchImpl appSearchImpl) {
         Objects.requireNonNull(appSearchImpl);
@@ -81,7 +83,7 @@
     /**
      * Gets storage usage byte size for a package with a given package name.
      *
-     * <p> Please note the storage info cached in file may be stale.
+     * <p>Please note the storage info cached in file may be stale.
      */
     public long getSizeBytesForPackage(@NonNull String packageName) {
         Objects.requireNonNull(packageName);
@@ -91,14 +93,15 @@
     /**
      * Gets total storage usage byte size for all packages under the user.
      *
-     * <p> Please note the storage info cached in file may be stale.
+     * <p>Please note the storage info cached in file may be stale.
      */
     public long getTotalSizeBytes() {
         return mTotalStorageSizeBytes;
     }
 
+    @RequiresNonNull("mStorageInfoFile")
     @VisibleForTesting
-    void readStorageInfoFromFile() {
+    void readStorageInfoFromFile(@UnderInitialization UserStorageInfo this) {
         if (mStorageInfoFile.exists()) {
             mReadWriteLock.readLock().lock();
             try (InputStream in = new FileInputStream(mStorageInfoFile)) {
@@ -122,7 +125,8 @@
     // calculation/interpolation logic.
     @NonNull
     @VisibleForTesting
-    Map<String, Long> calculatePackageStorageInfoMap(@NonNull StorageInfoProto storageInfo) {
+    Map<String, Long> calculatePackageStorageInfoMap(
+            @UnknownInitialization UserStorageInfo this, @NonNull StorageInfoProto storageInfo) {
         Map<String, Long> packageStorageInfoMap = new ArrayMap<>();
         if (storageInfo.hasDocumentStorageInfo()) {
             DocumentStorageInfoProto documentStorageInfo = storageInfo.getDocumentStorageInfo();
@@ -134,12 +138,13 @@
             for (int i = 0; i < namespaceStorageInfoList.size(); i++) {
                 NamespaceStorageInfoProto namespaceStorageInfo = namespaceStorageInfoList.get(i);
                 String packageName = getPackageName(namespaceStorageInfo.getNamespace());
-                int namespaceDocuments = namespaceStorageInfo.getNumAliveDocuments()
-                        + namespaceStorageInfo.getNumExpiredDocuments();
+                int namespaceDocuments =
+                        namespaceStorageInfo.getNumAliveDocuments()
+                                + namespaceStorageInfo.getNumExpiredDocuments();
                 totalDocuments += namespaceDocuments;
-                packageDocumentCountMap.put(packageName,
-                        packageDocumentCountMap.getOrDefault(packageName, 0)
-                                + namespaceDocuments);
+                packageDocumentCountMap.put(
+                        packageName,
+                        packageDocumentCountMap.getOrDefault(packageName, 0) + namespaceDocuments);
             }
 
             long totalStorageSize = storageInfo.getTotalStorageSize();
@@ -148,7 +153,8 @@
                 // Note that while the total storage takes into account schema, index, etc. in
                 // addition to documents, we'll only calculate the percentage based on number of
                 // documents under packages.
-                packageStorageInfoMap.put(entry.getKey(),
+                packageStorageInfoMap.put(
+                        entry.getKey(),
                         (long) (entry.getValue() * 1.0 / totalDocuments * totalStorageSize));
             }
         }
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppSearchHelper.java b/service/java/com/android/server/appsearch/appsindexer/AppSearchHelper.java
new file mode 100644
index 0000000..ef3dd52
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppSearchHelper.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchEnvironmentFactory;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.BatchResultCallback;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+import android.util.AndroidRuntimeException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import java.io.Closeable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Helper class to manage the App corpus in AppSearch.
+ *
+ * <p>There are two primary methods in this class, {@link #setSchemasForPackages} and {@link
+ * #indexApps}. On a given Apps Index update, they may not necessarily both be called. For instance,
+ * if the indexer determines that the only change is that an app was deleted, there is no reason to
+ * insert any * apps, so we can save time by only calling setSchemas to erase the deleted app
+ * schema. On the other hand, if the only change is that an app was update, there is no reason to
+ * call setSchema. We can instead just update the updated app with a call to indexApps. Figuring out
+ * what needs to be done is left to {@link AppsIndexerImpl}.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @hide
+ */
+public class AppSearchHelper implements Closeable {
+    private static final String TAG = "AppSearchAppsIndexerAppSearchHelper";
+
+    // The apps indexer uses one database, and in that database we have one schema for every app
+    // that is indexed. The reason for this is that we keep the schema types the same for every app
+    // (MobileApplication), but we need different visibility settings for each app. These different
+    // visibility settings are set with Public ACL and rely on PackageManager#canPackageQuery.
+    // Therefore each application needs its own schema. We put all these schema into a single
+    // database by dynamically renaming the schema so that they have different names.
+    public static final String APP_DATABASE = "apps-db";
+    private static final int GET_APP_IDS_PAGE_SIZE = 1000;
+    private final Context mContext;
+    private final ExecutorService mExecutor;
+    private final AppSearchManager mAppSearchManager;
+    private SyncAppSearchSession mSyncAppSearchSession;
+    private SyncGlobalSearchSession mSyncGlobalSearchSession;
+
+    /** Creates and initializes an {@link AppSearchHelper} */
+    @NonNull
+    public static AppSearchHelper createAppSearchHelper(@NonNull Context context)
+            throws AppSearchException {
+        Objects.requireNonNull(context);
+
+        AppSearchHelper appSearchHelper = new AppSearchHelper(context);
+        appSearchHelper.initializeAppSearchSessions();
+        return appSearchHelper;
+    }
+
+    /** Creates an initialized {@link AppSearchHelper}. */
+    @VisibleForTesting
+    private AppSearchHelper(@NonNull Context context) {
+        mContext = Objects.requireNonNull(context);
+
+        mAppSearchManager = context.getSystemService(AppSearchManager.class);
+        if (mAppSearchManager == null) {
+            throw new AndroidRuntimeException(
+                    "Can't get AppSearchManager to initialize AppSearchHelper.");
+        }
+        mExecutor =
+                AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor();
+    }
+
+    /**
+     * Sets up the search session.
+     *
+     * @throws AppSearchException if unable to initialize the {@link SyncAppSearchSession} or the
+     *     {@link SyncGlobalSearchSession}.
+     */
+    private void initializeAppSearchSessions() throws AppSearchException {
+        AppSearchManager.SearchContext searchContext =
+                new AppSearchManager.SearchContext.Builder(APP_DATABASE).build();
+        mSyncAppSearchSession =
+                new SyncAppSearchSessionImpl(mAppSearchManager, searchContext, mExecutor);
+        mSyncGlobalSearchSession = new SyncGlobalSearchSessionImpl(mAppSearchManager, mExecutor);
+    }
+
+    /** Just for testing, allows us to test various scenarios involving SyncAppSearchSession. */
+    @VisibleForTesting
+    /* package */ void setAppSearchSession(@NonNull SyncAppSearchSession session) {
+        // Close the old one
+        mSyncAppSearchSession.close();
+        mSyncAppSearchSession = Objects.requireNonNull(session);
+    }
+
+    /**
+     * Sets the AppsIndexer database schema to correspond to the list of passed in {@link
+     * PackageIdentifier}s. Note that this means if a schema exists in AppSearch that does not get
+     * passed in to this method, it will be erased. And if a schema does not exist in AppSearch that
+     * is passed in to this method, it will be created.
+     */
+    public void setSchemasForPackages(@NonNull List<PackageIdentifier> pkgs)
+            throws AppSearchException {
+        Objects.requireNonNull(pkgs);
+        SetSchemaRequest.Builder schemaBuilder =
+                new SetSchemaRequest.Builder()
+                        // If MobileApplication schema later gets changed to a compatible schema, we
+                        // should first try setting the schema with forceOverride = false.
+                        .setForceOverride(true);
+        for (int i = 0; i < pkgs.size(); i++) {
+            PackageIdentifier pkg = pkgs.get(i);
+            // As all apps are in the same db, we have to make sure that even if it's getting
+            // updated, the schema is in the list of schemas
+            String packageName = pkg.getPackageName();
+            AppSearchSchema schemaVariant =
+                    MobileApplication.createMobileApplicationSchemaForPackage(packageName);
+            schemaBuilder.addSchemas(schemaVariant);
+            // Since the Android package of the underlying apps are different from the package name
+            // that "owns" the builtin:MobileApplication corpus in AppSearch, we needed to add the
+            // PackageIdentifier parameter to setPubliclyVisibleSchema.
+            schemaBuilder.setPubliclyVisibleSchema(schemaVariant.getSchemaType(), pkg);
+        }
+
+        // TODO(b/275592563): Log app removal in metrics
+        mSyncAppSearchSession.setSchema(schemaBuilder.build());
+    }
+
+    /**
+     * Indexes a collection of apps into AppSearch. This requires that the corresponding
+     * MobileApplication schemas are already set by a previous call to {@link
+     * #setSchemasForPackages}. The call doesn't necessarily have to happen in the current sync.
+     *
+     * @throws AppSearchException if indexing results in a {@link
+     *     AppSearchResult#RESULT_OUT_OF_SPACE} result code. It will also throw this if the put call
+     *     results in a system error as in {@link BatchResultCallback#onSystemError}. This may
+     *     happen if the AppSearch service unexpectedly fails to initialize and can't be recovered,
+     *     for instance.
+     */
+    public void indexApps(@NonNull List<MobileApplication> apps) throws AppSearchException {
+        Objects.requireNonNull(apps);
+
+        // At this point, the document schema names have already been set to the per-package name.
+        // We can just add them to the request.
+        PutDocumentsRequest request =
+                new PutDocumentsRequest.Builder().addGenericDocuments(apps).build();
+
+        AppSearchBatchResult<String, Void> result = mSyncAppSearchSession.put(request);
+        if (!result.isSuccess()) {
+            Map<String, AppSearchResult<Void>> failures = result.getFailures();
+            for (AppSearchResult<Void> failure : failures.values()) {
+                // If it's out of space, stop indexing
+                if (failure.getResultCode() == AppSearchResult.RESULT_OUT_OF_SPACE) {
+                    throw new AppSearchException(
+                            failure.getResultCode(), failure.getErrorMessage());
+                } else {
+                    Log.e(TAG, "Ran into error while indexing apps: " + failure);
+                }
+            }
+        }
+    }
+
+    /**
+     * Searches AppSearch and returns a Map with the package ids and their last updated times. This
+     * helps us determine which app documents need to be re-indexed.
+     */
+    @NonNull
+    public Map<String, Long> getAppsFromAppSearch() {
+        SearchSpec allAppsSpec =
+                new SearchSpec.Builder()
+                        .addFilterNamespaces(MobileApplication.APPS_NAMESPACE)
+                        .addProjection(
+                                SearchSpec.SCHEMA_TYPE_WILDCARD,
+                                Collections.singletonList(
+                                        MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP))
+                        .addFilterPackageNames(mContext.getPackageName())
+                        .setResultCountPerPage(GET_APP_IDS_PAGE_SIZE)
+                        .build();
+        SyncSearchResults results = mSyncGlobalSearchSession.search(/* query= */ "", allAppsSpec);
+        return collectUpdatedTimestampFromAllPages(results);
+    }
+
+    /** Iterates through result pages to get the last updated times */
+    @NonNull
+    private Map<String, Long> collectUpdatedTimestampFromAllPages(
+            @NonNull SyncSearchResults results) {
+        Objects.requireNonNull(results);
+        Map<String, Long> appUpdatedMap = new ArrayMap<>();
+
+        try {
+            List<SearchResult> resultList = results.getNextPage();
+
+            while (!resultList.isEmpty()) {
+                for (int i = 0; i < resultList.size(); i++) {
+                    SearchResult result = resultList.get(i);
+                    appUpdatedMap.put(
+                            result.getGenericDocument().getId(),
+                            result.getGenericDocument()
+                                    .getPropertyLong(
+                                            MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP));
+                }
+
+                resultList = results.getNextPage();
+            }
+        } catch (AppSearchException e) {
+            Log.e(TAG, "Error while searching for all app documents", e);
+        }
+        // Return what we have so far. Even if this doesn't fetch all documents, that is fine as we
+        // can continue with indexing. The documents that aren't fetched will be detected as new
+        // apps and re-indexed.
+        return appUpdatedMap;
+    }
+
+    /** Closes the AppSearch sessions. */
+    @Override
+    public void close() {
+        mSyncAppSearchSession.close();
+        mSyncGlobalSearchSession.close();
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsIndexerConfig.java b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerConfig.java
new file mode 100644
index 0000000..9ab105a
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerConfig.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An interface which exposes config flags to Apps Indexer.
+ *
+ * <p>Implementations of this interface must be thread-safe.
+ *
+ * @hide
+ */
+public interface AppsIndexerConfig {
+    boolean DEFAULT_APPS_INDEXER_ENABLED = false;
+    long DEFAULT_APPS_UPDATE_INTERVAL_MILLIS = TimeUnit.DAYS.toMillis(30); // 30 days.
+
+    /** Returns whether Apps Indexer is enabled. */
+    boolean isAppsIndexerEnabled();
+
+    /* Returns the minimum internal in millis for two consecutive scheduled updates. */
+    long getAppsMaintenanceUpdateIntervalMillis();
+}
+
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsIndexerImpl.java b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerImpl.java
new file mode 100644
index 0000000..7947cdd
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerImpl.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Interactions with PackageManager and AppSearch.
+ *
+ * <p>This class is NOT thread-safe.
+ *
+ * @hide
+ */
+public final class AppsIndexerImpl implements Closeable {
+    static final String TAG = "AppSearchAppsIndexerImpl";
+
+    private final Context mContext;
+    private final AppSearchHelper mAppSearchHelper;
+
+    public AppsIndexerImpl(@NonNull Context context) throws AppSearchException {
+        mContext = Objects.requireNonNull(context);
+        mAppSearchHelper = AppSearchHelper.createAppSearchHelper(context);
+    }
+
+    /**
+     * Checks PackageManager and AppSearch to sync the Apps Index in AppSearch.
+     *
+     * <p>It deletes removed apps, inserts newly-added ones, and updates existing ones in the App
+     * corpus in AppSearch.
+     *
+     * @param settings contains update timestamps that help the indexer determine which apps were
+     *     updated.
+     */
+    @VisibleForTesting
+    public void doUpdate(@NonNull AppsIndexerSettings settings) throws AppSearchException {
+        Objects.requireNonNull(settings);
+        long currentTimeMillis = System.currentTimeMillis();
+
+        PackageManager packageManager = mContext.getPackageManager();
+
+        // Search AppSearch for MobileApplication objects to get a "current" list of indexed apps.
+        Map<String, Long> appUpdatedTimestamps = mAppSearchHelper.getAppsFromAppSearch();
+        Map<PackageInfo, ResolveInfo> launchablePackages =
+                AppsUtil.getLaunchablePackages(packageManager);
+        Set<PackageInfo> packageInfos = launchablePackages.keySet();
+
+        Map<PackageInfo, ResolveInfo> packagesToBeAddedOrUpdated = new ArrayMap<>();
+        long mostRecentAppUpdatedTimestampMillis = settings.getLastAppUpdateTimestampMillis();
+
+        // Prepare a set of current app IDs for efficient lookup
+        Set<String> currentAppIds = new ArraySet<>();
+        for (PackageInfo packageInfo : packageInfos) {
+            currentAppIds.add(packageInfo.packageName);
+
+            // Update the most recent timestamp as we iterate
+            if (packageInfo.lastUpdateTime > mostRecentAppUpdatedTimestampMillis) {
+                mostRecentAppUpdatedTimestampMillis = packageInfo.lastUpdateTime;
+            }
+
+            Long storedUpdateTime = appUpdatedTimestamps.get(packageInfo.packageName);
+
+            if (storedUpdateTime == null || packageInfo.lastUpdateTime != storedUpdateTime) {
+                // Added or updated
+                packagesToBeAddedOrUpdated.put(packageInfo, launchablePackages.get(packageInfo));
+            }
+        }
+
+        try {
+            if (!currentAppIds.equals(appUpdatedTimestamps.keySet())) {
+                // The current list of apps in AppSearch does not match what is in PackageManager.
+                // This means this is the first sync, an app was removed, or an app was added. In
+                // all cases, we need to call setSchema to keep AppSearch in sync with
+                // PackageManager.
+                List<PackageIdentifier> packageIdentifiers = new ArrayList<>();
+                for (PackageInfo packageInfo : packageInfos) {
+                    // We get certificates here as getting the certificates during the previous for
+                    // loop would be wasteful if we end up not needing to call set schema
+                    byte[] certificate = AppsUtil.getCertificate(packageInfo);
+                    if (certificate == null) {
+                        Log.e(TAG, "Certificate not found for package: " + packageInfo.packageName);
+                        continue;
+                    }
+                    packageIdentifiers.add(
+                            new PackageIdentifier(packageInfo.packageName, certificate));
+                }
+                // The certificate is necessary along with the package name as it is used in
+                // visibility settings.
+                mAppSearchHelper.setSchemasForPackages(packageIdentifiers);
+            }
+
+            if (!packagesToBeAddedOrUpdated.isEmpty()) {
+                mAppSearchHelper.indexApps(
+                        AppsUtil.buildAppsFromPackageInfos(
+                                packageManager, packagesToBeAddedOrUpdated));
+            }
+
+            settings.setLastAppUpdateTimestampMillis(mostRecentAppUpdatedTimestampMillis);
+            settings.setLastUpdateTimestampMillis(currentTimeMillis);
+        } catch (AppSearchException e) {
+            // Reset the last update time stamp and app update timestamp so we can try again later.
+            settings.reset();
+            throw e;
+        }
+    }
+
+    /** Shuts down the {@link AppsIndexerImpl} and its {@link AppSearchHelper}. */
+    @Override
+    public void close() {
+        mAppSearchHelper.close();
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsIndexerMaintenanceConfig.java b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerMaintenanceConfig.java
new file mode 100644
index 0000000..6d727a9
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerMaintenanceConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.indexer.IndexerLocalService;
+import com.android.server.appsearch.indexer.IndexerMaintenanceConfig;
+
+/** Singleton class containing configuration for the apps indexer maintenance task. */
+public class AppsIndexerMaintenanceConfig implements IndexerMaintenanceConfig {
+    @VisibleForTesting
+    static final int MIN_APPS_INDEXER_JOB_ID = 16964307; // Contacts Indexer Max Job Id + 1
+
+    public static final IndexerMaintenanceConfig INSTANCE = new AppsIndexerMaintenanceConfig();
+
+    /** Enforces singleton class pattern. */
+    private AppsIndexerMaintenanceConfig() {}
+
+    @NonNull
+    @Override
+    public Class<? extends IndexerLocalService> getLocalService() {
+        return AppsIndexerManagerService.LocalService.class;
+    }
+
+    @Override
+    public int getMinJobId() {
+        return MIN_APPS_INDEXER_JOB_ID;
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsIndexerManagerService.java b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerManagerService.java
new file mode 100644
index 0000000..9377dc9
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerManagerService.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static android.os.Process.INVALID_UID;
+
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchEnvironment;
+import android.app.appsearch.AppSearchEnvironmentFactory;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.util.LogUtil;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.CancellationSignal;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.SystemService;
+import com.android.server.appsearch.indexer.IndexerLocalService;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Manages the per device-user AppsIndexer instance to index apps into AppSearch.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @hide
+ */
+public final class AppsIndexerManagerService extends SystemService {
+    private static final String TAG = "AppSearchAppsIndexerManagerS";
+
+    private final Context mContext;
+    private final LocalService mLocalService;
+
+    // Map of AppsIndexerUserInstances indexed by the UserHandle
+    @GuardedBy("mAppsIndexersLocked")
+    private final Map<UserHandle, AppsIndexerUserInstance> mAppsIndexersLocked = new ArrayMap<>();
+
+    private final AppsIndexerConfig mAppsIndexerConfig;
+
+    /** Constructs a {@link AppsIndexerManagerService}. */
+    public AppsIndexerManagerService(
+            @NonNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig) {
+        super(context);
+        mContext = Objects.requireNonNull(context);
+        mAppsIndexerConfig = Objects.requireNonNull(appsIndexerConfig);
+        mLocalService = new LocalService();
+    }
+
+    @Override
+    public void onStart() {
+        registerReceivers();
+        LocalManagerRegistry.addManager(LocalService.class, mLocalService);
+    }
+
+    /** Runs when a user is unlocked. This will attempt to run an initial sync. */
+    @Override
+    public void onUserUnlocking(@NonNull TargetUser user) {
+        try {
+            Objects.requireNonNull(user);
+            UserHandle userHandle = user.getUserHandle();
+            synchronized (mAppsIndexersLocked) {
+                AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle);
+                if (instance == null) {
+                    AppSearchEnvironment appSearchEnvironment =
+                            AppSearchEnvironmentFactory.getEnvironmentInstance();
+                    Context userContext =
+                            appSearchEnvironment.createContextAsUser(mContext, userHandle);
+                    File appSearchDir =
+                            appSearchEnvironment.getAppSearchDir(userContext, userHandle);
+                    File appsDir = new File(appSearchDir, "apps");
+                    instance =
+                            AppsIndexerUserInstance.createInstance(
+                                    userContext, appsDir, mAppsIndexerConfig);
+                    if (LogUtil.DEBUG) {
+                        Log.d(TAG, "Created Apps Indexer instance for user " + userHandle);
+                    }
+                    mAppsIndexersLocked.put(userHandle, instance);
+                }
+
+                instance.updateAsync(/* firstRun= */ true);
+            }
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "AppsIndexerManagerService.onUserUnlocking() failed ", e);
+        } catch (AppSearchException e) {
+            Log.e(TAG, "Error while start Apps Indexer", e);
+        }
+    }
+
+    /** Handles user stopping by shutting down the instance for the user. */
+    @Override
+    public void onUserStopping(@NonNull TargetUser user) {
+        try {
+            Objects.requireNonNull(user);
+            UserHandle userHandle = user.getUserHandle();
+            synchronized (mAppsIndexersLocked) {
+                AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle);
+                if (instance != null) {
+                    mAppsIndexersLocked.remove(userHandle);
+                    try {
+                        instance.shutdown();
+                    } catch (InterruptedException e) {
+                        Log.w(TAG, "Failed to shutdown apps indexer for " + userHandle, e);
+                    }
+                }
+            }
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "AppsIndexerManagerService.onUserStopping() failed ", e);
+        }
+    }
+
+    /** Dumps AppsIndexer internal state for the user. */
+    @BinderThread
+    public void dumpAppsIndexerForUser(@NonNull UserHandle userHandle, @NonNull PrintWriter pw) {
+        try {
+            Objects.requireNonNull(userHandle);
+            Objects.requireNonNull(pw);
+            synchronized (mAppsIndexersLocked) {
+                AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle);
+                if (instance != null) {
+                    instance.dump(pw);
+                } else {
+                    pw.println("AppsIndexerUserInstance is not created for " + userHandle);
+                }
+            }
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "AppsIndexerManagerService.dumpAppsIndexerForUser() failed ", e);
+        }
+    }
+
+    /**
+     * Registers a broadcast receiver to get package changed (disabled/enabled) and package data
+     * cleared events.
+     */
+    private void registerReceivers() {
+        IntentFilter appChangedFilter = new IntentFilter();
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        appChangedFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
+        appChangedFilter.addDataScheme("package");
+
+        mContext.registerReceiverForAllUsers(
+                new AppsProviderChangedReceiver(),
+                appChangedFilter,
+                /* broadcastPermission= */ null,
+                /* scheduler= */ null);
+        if (LogUtil.DEBUG) {
+            Log.v(TAG, "Registered receiver for package events");
+        }
+    }
+
+    /**
+     * Broadcast receiver to handle package events and index them into the AppSearch
+     * "builtin:MobileApplication" schema.
+     *
+     * <p>This broadcast receiver allows the apps indexer to listen to events which indicate that
+     * app info was changed.
+     */
+    private class AppsProviderChangedReceiver extends BroadcastReceiver {
+
+        /**
+         * Checks if the entire package was changed, or if the intent just represents a component
+         * change.
+         */
+        private boolean isEntirePackageChanged(@NonNull Intent intent) {
+            Objects.requireNonNull(intent);
+            String[] changedComponents =
+                    intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+            if (changedComponents == null) {
+                Log.e(TAG, "Received ACTION_PACKAGE_CHANGED event with null changed components");
+                return false;
+            }
+            if (intent.getData() == null) {
+                Log.e(TAG, "Received ACTION_PACKAGE_CHANGED event with null data");
+                return false;
+            }
+            String changedPackage = intent.getData().getSchemeSpecificPart();
+            for (int i = 0; i < changedComponents.length; i++) {
+                String changedComponent = changedComponents[i];
+                // If the state of the overall package has changed, then it will contain
+                // an entry with the package name itself.
+                if (changedComponent.equals(changedPackage)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /** Handles intents related to package changes. */
+        @Override
+        public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+            try {
+                Objects.requireNonNull(context);
+                Objects.requireNonNull(intent);
+
+                switch (intent.getAction()) {
+                    case Intent.ACTION_PACKAGE_CHANGED:
+                        if (!isEntirePackageChanged(intent)) {
+                            // If it was just a component change, do not run the indexer
+                            return;
+                        }
+                        // fall through
+                    case Intent.ACTION_PACKAGE_ADDED:
+                    case Intent.ACTION_PACKAGE_REPLACED:
+                    case Intent.ACTION_PACKAGE_FULLY_REMOVED:
+                        // TODO(b/275592563): handle more efficiently based on package event type
+                        // TODO(b/275592563): determine if batching is necessary in the case of
+                        //  rapid updates
+
+                        int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+                        if (uid == INVALID_UID) {
+                            Log.w(TAG, "uid is missing in the intent: " + intent);
+                            return;
+                        }
+                        Log.d(TAG, "userid in package receiver: " + uid);
+                        UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+                        mLocalService.doUpdateForUser(userHandle, /* unused= */ null);
+                        break;
+                    default:
+                        Log.w(TAG, "Received unknown intent: " + intent);
+                }
+            } catch (RuntimeException e) {
+                Slog.wtf(TAG, "AppsProviderChangedReceiver.onReceive() failed ", e);
+            }
+        }
+    }
+
+    public class LocalService implements IndexerLocalService {
+        /** Runs an update for a user. */
+        @Override
+        public void doUpdateForUser(
+                @NonNull UserHandle userHandle, @Nullable CancellationSignal unused) {
+            // TODO(b/275592563): handle cancellation signal to abort the job.
+            Objects.requireNonNull(userHandle);
+            synchronized (mAppsIndexersLocked) {
+                AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle);
+                if (instance != null) {
+                    instance.updateAsync(/* firstRun= */ false);
+                }
+            }
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsIndexerSettings.java b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerSettings.java
new file mode 100644
index 0000000..28c1daf
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerSettings.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Apps indexer settings backed by a PersistableBundle.
+ *
+ * <p>Holds settings such as:
+ *
+ * <ul>
+ *   <li>the last time a full update was performed
+ *   <li>the time of the last apps update
+ *   <li>the time of the last apps deletion
+ * </ul>
+ *
+ * <p>This class is NOT thread safe (similar to {@link PersistableBundle} which it wraps).
+ *
+ * @hide
+ */
+public class AppsIndexerSettings {
+    static final String SETTINGS_FILE_NAME = "apps_indexer_settings.pb";
+    static final String LAST_UPDATE_TIMESTAMP_KEY = "last_update_timestamp_millis";
+    static final String LAST_APP_UPDATE_TIMESTAMP_KEY = "last_app_update_timestamp_millis";
+
+    private final File mFile;
+    private PersistableBundle mBundle = new PersistableBundle();
+
+    public AppsIndexerSettings(@NonNull File baseDir) {
+        Objects.requireNonNull(baseDir);
+        mFile = new File(baseDir, SETTINGS_FILE_NAME);
+    }
+
+    public void load() throws IOException {
+        mBundle = readBundle(mFile);
+    }
+
+    public void persist() throws IOException {
+        writeBundle(mFile, mBundle);
+    }
+
+    /** Returns the timestamp of when the last full update occurred in milliseconds. */
+    public long getLastUpdateTimestampMillis() {
+        return mBundle.getLong(LAST_UPDATE_TIMESTAMP_KEY);
+    }
+
+    /** Sets the timestamp of when the last full update occurred in milliseconds. */
+    public void setLastUpdateTimestampMillis(long timestampMillis) {
+        mBundle.putLong(LAST_UPDATE_TIMESTAMP_KEY, timestampMillis);
+    }
+
+    /** Returns the timestamp of when the last app was updated in milliseconds. */
+    public long getLastAppUpdateTimestampMillis() {
+        return mBundle.getLong(LAST_APP_UPDATE_TIMESTAMP_KEY);
+    }
+
+    /** Sets the timestamp of when the last apps was updated in milliseconds. */
+    public void setLastAppUpdateTimestampMillis(long timestampMillis) {
+        mBundle.putLong(LAST_APP_UPDATE_TIMESTAMP_KEY, timestampMillis);
+    }
+
+    /** Resets all the settings to default values. */
+    public void reset() {
+        setLastUpdateTimestampMillis(0);
+        setLastAppUpdateTimestampMillis(0);
+    }
+
+    @VisibleForTesting
+    @NonNull
+    static PersistableBundle readBundle(@NonNull File src) throws IOException {
+        AtomicFile atomicFile = new AtomicFile(src);
+        try (FileInputStream fis = atomicFile.openRead()) {
+            return PersistableBundle.readFromStream(fis);
+        }
+    }
+
+    @VisibleForTesting
+    static void writeBundle(@NonNull File dest, @NonNull PersistableBundle bundle)
+            throws IOException {
+        AtomicFile atomicFile = new AtomicFile(dest);
+        FileOutputStream fos = null;
+        try {
+            fos = atomicFile.startWrite();
+            bundle.writeToStream(fos);
+            atomicFile.finishWrite(fos);
+        } catch (IOException e) {
+            if (fos != null) {
+                atomicFile.failWrite(fos);
+            }
+            throw e;
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsIndexerUserInstance.java b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerUserInstance.java
new file mode 100644
index 0000000..13f7b61
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsIndexerUserInstance.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.APPS_INDEXER;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchEnvironmentFactory;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Apps Indexer for a single user.
+ *
+ * <p>It reads the updated/newly-inserted/deleted apps from PackageManager, and syncs the changes
+ * into AppSearch.
+ *
+ * <p>This class is thread safe.
+ *
+ * @hide
+ */
+public final class AppsIndexerUserInstance {
+
+    private static final String TAG = "AppSearchAppsIndexerUserInst";
+
+    private final File mDataDir;
+    // While AppsIndexerSettings is not thread safe, it is only accessed through a single-threaded
+    // executor service. It will be read and updated before the next scheduled task accesses it.
+    private final AppsIndexerSettings mSettings;
+
+    // Used for handling the app change notification so we won't schedule too many updates. At any
+    // time, only two threads can run an update. But since we use a single-threaded executor, it
+    // means that at most one thread can be running, and another thread can be waiting to run. This
+    // will happen in the case that an update is requested while another is running.
+    private final Semaphore mRunningOrScheduledSemaphore = new Semaphore(2);
+
+    private AppsIndexerImpl mAppsIndexerImpl;
+
+    /**
+     * Single threaded executor to make sure there is only one active sync for this {@link
+     * AppsIndexerUserInstance}. Background tasks should be scheduled using {@link
+     * #executeOnSingleThreadedExecutor(Runnable)} which ensures that they are not executed if the
+     * executor is shutdown during {@link #shutdown()}.
+     *
+     * <p>Note that this executor is used as both work and callback executors which is fine because
+     * AppSearch should be able to handle exceptions thrown by them.
+     */
+    private final ExecutorService mSingleThreadedExecutor;
+
+    private final Context mContext;
+    private final AppsIndexerConfig mAppsIndexerConfig;
+
+    /**
+     * Constructs and initializes a {@link AppsIndexerUserInstance}.
+     *
+     * <p>Heavy operations such as connecting to AppSearch are performed asynchronously.
+     *
+     * @param appsDir data directory for AppsIndexer.
+     */
+    @NonNull
+    public static AppsIndexerUserInstance createInstance(
+            @NonNull Context userContext,
+            @NonNull File appsDir,
+            @NonNull AppsIndexerConfig appsIndexerConfig)
+            throws AppSearchException {
+        Objects.requireNonNull(userContext);
+        Objects.requireNonNull(appsDir);
+        Objects.requireNonNull(appsIndexerConfig);
+
+        ExecutorService singleThreadedExecutor =
+                AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor();
+        return createInstance(userContext, appsDir, appsIndexerConfig, singleThreadedExecutor);
+    }
+
+    @VisibleForTesting
+    @NonNull
+    static AppsIndexerUserInstance createInstance(
+            @NonNull Context context,
+            @NonNull File appsDir,
+            @NonNull AppsIndexerConfig appsIndexerConfig,
+            @NonNull ExecutorService executorService)
+            throws AppSearchException {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(appsDir);
+        Objects.requireNonNull(appsIndexerConfig);
+        Objects.requireNonNull(executorService);
+
+        AppsIndexerUserInstance indexer =
+                new AppsIndexerUserInstance(appsDir, executorService, context, appsIndexerConfig);
+        indexer.loadSettingsAsync();
+        indexer.mAppsIndexerImpl = new AppsIndexerImpl(context);
+
+        return indexer;
+    }
+
+    /**
+     * Constructs a {@link AppsIndexerUserInstance}.
+     *
+     * @param dataDir data directory for storing apps indexer state.
+     * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure
+     *     the thread safety of this class.
+     * @param context Context object passed from {@link AppsIndexerManagerService}
+     */
+    private AppsIndexerUserInstance(
+            @NonNull File dataDir,
+            @NonNull ExecutorService singleThreadedExecutor,
+            @NonNull Context context,
+            @NonNull AppsIndexerConfig appsIndexerConfig) {
+        mDataDir = Objects.requireNonNull(dataDir);
+        mSettings = new AppsIndexerSettings(mDataDir);
+        mSingleThreadedExecutor = Objects.requireNonNull(singleThreadedExecutor);
+        mContext = Objects.requireNonNull(context);
+        mAppsIndexerConfig = Objects.requireNonNull(appsIndexerConfig);
+    }
+
+    /** Shuts down the AppsIndexerUserInstance */
+    public void shutdown() throws InterruptedException {
+        mAppsIndexerImpl.close();
+        IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                mContext, mContext.getUser(), APPS_INDEXER);
+        synchronized (mSingleThreadedExecutor) {
+            mSingleThreadedExecutor.shutdown();
+        }
+        boolean unused = mSingleThreadedExecutor.awaitTermination(30L, TimeUnit.SECONDS);
+    }
+
+    /** Dumps the internal state of this {@link AppsIndexerUserInstance}. */
+    public void dump(@NonNull PrintWriter pw) {
+        // Those timestamps are not protected by any lock since in AppsIndexerUserInstance
+        // we only have one thread to handle all the updates. It is possible we might run into
+        // race condition if there is an update running while those numbers are being printed.
+        // This is acceptable though for debug purpose, so still no lock here.
+        pw.println("last_update_timestamp_millis: " + mSettings.getLastUpdateTimestampMillis());
+        pw.println(
+                "last_app_update_timestamp_millis: " + mSettings.getLastAppUpdateTimestampMillis());
+    }
+
+    /**
+     * Schedule an update. No new update can be scheduled if there are two updates already scheduled
+     * or currently being run.
+     *
+     * @param firstRun boolean indicating if this is a first run and that settings should be checked
+     *     for the last update timestamp.
+     */
+    public void updateAsync(boolean firstRun) {
+        // Try to acquire a permit.
+        if (!mRunningOrScheduledSemaphore.tryAcquire()) {
+            // If there are none available, that means an update is running and we have ALREADY
+            // received a change mid-update. The third update request was received during the first
+            // update, and will be handled by the scheduled update.
+            return;
+        }
+        // If there is a permit available, that cold mean there is one update running right now
+        // with none scheduled. Since we use a single threaded executor, calling execute on it
+        // right now will run the requested update after the current update. It could also mean
+        // there is no update running right now, so we can just call execute and run the update
+        // right now.
+        executeOnSingleThreadedExecutor(
+                () -> {
+                    doUpdate(firstRun);
+                    IndexerMaintenanceService.scheduleUpdateJob(
+                            mContext,
+                            mContext.getUser(),
+                            APPS_INDEXER,
+                            /* periodic= */ true,
+                            /* intervalMillis= */ mAppsIndexerConfig
+                                    .getAppsMaintenanceUpdateIntervalMillis());
+                });
+    }
+
+    /**
+     * Does the update. It also releases a permit from {@link #mRunningOrScheduledSemaphore}
+     *
+     * @param firstRun when set to true, that means this was called from onUserUnlocking. If we
+     *     didn't have this check, the apps indexer would run every time the phone got unlocked. It
+     *     should only run the first time this happens.
+     */
+    @VisibleForTesting
+    void doUpdate(boolean firstRun) {
+        try {
+            // Check if there was a prior run
+            if (firstRun && mSettings.getLastUpdateTimestampMillis() != 0) {
+                return;
+            }
+            mAppsIndexerImpl.doUpdate(mSettings);
+            mSettings.persist();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to save settings to disk", e);
+        } catch (AppSearchException e) {
+            Log.e(TAG, "Failed to sync Apps to AppSearch", e);
+        } finally {
+            // Finish a update. If there were no permits available, the update that was requested
+            // mid-update will run. If there was one permit available, we won't run another update.
+            // This happens if no updates were scheduled during the update.
+            mRunningOrScheduledSemaphore.release();
+        }
+    }
+
+    /**
+     * Loads the persisted data from disk.
+     *
+     * <p>It doesn't throw here. If it fails to load file, AppsIndexer would always use the
+     * timestamps persisted in the memory.
+     */
+    private void loadSettingsAsync() {
+        executeOnSingleThreadedExecutor(
+                () -> {
+                    try {
+                        // If the directory already exists, this returns false. That is fine as it
+                        // might not be the first sync. If this returns true, that is fine as it is
+                        // the first run and we want to make a new directory.
+                        mDataDir.mkdirs();
+                    } catch (SecurityException e) {
+                        Log.e(TAG, "Failed to create settings directory on disk.", e);
+                        return;
+                    }
+
+                    try {
+                        mSettings.load();
+                    } catch (IOException e) {
+                        // Ignore file not found errors (bootstrap case)
+                        if (!(e instanceof FileNotFoundException)) {
+                            Log.e(TAG, "Failed to load settings from disk", e);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Executes the given command on {@link #mSingleThreadedExecutor} if it is still alive.
+     *
+     * <p>If the {@link #mSingleThreadedExecutor} has been shutdown, this method doesn't execute the
+     * given command, and returns silently. Specifically, it does not throw {@link
+     * java.util.concurrent.RejectedExecutionException}.
+     *
+     * @param command the runnable task
+     */
+    private void executeOnSingleThreadedExecutor(Runnable command) {
+        synchronized (mSingleThreadedExecutor) {
+            if (mSingleThreadedExecutor.isShutdown()) {
+                Log.w(TAG, "Executor is shutdown, not executing task");
+                return;
+            }
+            mSingleThreadedExecutor.execute(
+                    () -> {
+                        try {
+                            command.run();
+                        } catch (RuntimeException e) {
+                            Slog.wtf(
+                                    TAG,
+                                    "AppsIndexerUserInstance"
+                                            + ".executeOnSingleThreadedExecutor() failed ",
+                                    e);
+                        }
+                    });
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java b/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java
new file mode 100644
index 0000000..d77a1c7
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.util.LogUtil;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** Utility class for pulling apps details from package manager. */
+public final class AppsUtil {
+    public static final String TAG = "AppSearchAppsUtil";
+
+    private AppsUtil() {}
+
+    /** Gets the resource Uri given a resource id. */
+    @NonNull
+    private static Uri getResourceUri(
+            @NonNull PackageManager packageManager,
+            @NonNull ApplicationInfo appInfo,
+            int resourceId)
+            throws PackageManager.NameNotFoundException {
+        Objects.requireNonNull(packageManager);
+        Objects.requireNonNull(appInfo);
+        Resources resources = packageManager.getResourcesForApplication(appInfo);
+        String resPkg = resources.getResourcePackageName(resourceId);
+        String type = resources.getResourceTypeName(resourceId);
+        return makeResourceUri(appInfo.packageName, resPkg, type, resourceId);
+    }
+
+    /**
+     * Appends the resource id instead of name to make the resource uri due to b/161564466. The
+     * resource names for some apps (e.g. Chrome) are obfuscated due to resource name collapsing, so
+     * we need to use resource id instead.
+     *
+     * @see Uri
+     */
+    @NonNull
+    private static Uri makeResourceUri(
+            @NonNull String appPkg, @NonNull String resPkg, @NonNull String type, int resourceId) {
+        Objects.requireNonNull(appPkg);
+        Objects.requireNonNull(resPkg);
+        Objects.requireNonNull(type);
+
+        // For more details on Android URIs, see the official Android documentation:
+        // https://developer.android.com/guide/topics/providers/content-provider-basics#ContentURIs
+        Uri.Builder uriBuilder = new Uri.Builder();
+        uriBuilder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE);
+        uriBuilder.encodedAuthority(appPkg);
+        uriBuilder.appendEncodedPath(type);
+        if (!appPkg.equals(resPkg)) {
+            uriBuilder.appendEncodedPath(resPkg + ":" + resourceId);
+        } else {
+            uriBuilder.appendEncodedPath(String.valueOf(resourceId));
+        }
+        return uriBuilder.build();
+    }
+
+    /**
+     * Gets the icon uri for the activity.
+     *
+     * @return the icon Uri string, or null if there is no icon resource.
+     */
+    @Nullable
+    private static String getActivityIconUriString(
+            @NonNull PackageManager packageManager, @NonNull ActivityInfo activityInfo) {
+        Objects.requireNonNull(packageManager);
+        Objects.requireNonNull(activityInfo);
+        int iconResourceId = activityInfo.getIconResource();
+        if (iconResourceId == 0) {
+            return null;
+        }
+
+        try {
+            return getResourceUri(packageManager, activityInfo.applicationInfo, iconResourceId)
+                    .toString();
+        } catch (PackageManager.NameNotFoundException e) {
+            // If resources aren't found for the application, that is fine. We return null and
+            // handle it with getActivityIconUriString
+            return null;
+        }
+    }
+
+    /**
+     * Gets {@link PackageInfo}s only for packages that have a launch activity, along with their
+     * corresponding {@link ResolveInfo}. This is useful for building schemas as well as determining
+     * which packages to set schemas for.
+     *
+     * @return a mapping of {@link PackageInfo}s with their corresponding {@link ResolveInfo} for
+     *     the packages launch activity.
+     * @see PackageManager#getInstalledPackages
+     * @see PackageManager#queryIntentActivities
+     */
+    @NonNull
+    public static Map<PackageInfo, ResolveInfo> getLaunchablePackages(
+            @NonNull PackageManager packageManager) {
+        Objects.requireNonNull(packageManager);
+        List<PackageInfo> packageInfos =
+                packageManager.getInstalledPackages(
+                        PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES);
+        Map<PackageInfo, ResolveInfo> launchablePackages = new ArrayMap<>();
+        Intent intent = new Intent(Intent.ACTION_MAIN, null);
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        intent.setPackage(null);
+        List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
+        Map<String, ResolveInfo> packageNameToLauncher = new ArrayMap<>();
+        for (int i = 0; i < activities.size(); i++) {
+            ResolveInfo ri = activities.get(i);
+            packageNameToLauncher.put(ri.activityInfo.packageName, ri);
+        }
+
+        for (int i = 0; i < packageInfos.size(); i++) {
+            PackageInfo packageInfo = packageInfos.get(i);
+            ResolveInfo resolveInfo = packageNameToLauncher.get(packageInfo.packageName);
+            if (resolveInfo != null) {
+                // Include the resolve info as we might need it later to build the MobileApplication
+                launchablePackages.put(packageInfo, resolveInfo);
+            }
+        }
+
+        return launchablePackages;
+    }
+
+    /**
+     * Uses {@link PackageManager} and a Map of {@link PackageInfo}s to {@link ResolveInfo}s to
+     * build AppSearch {@link MobileApplication} documents. Info from both are required to build app
+     * documents.
+     *
+     * @param packageInfos a mapping of {@link PackageInfo}s and their corresponding {@link
+     *     ResolveInfo} for the packages launch activity.
+     */
+    @NonNull
+    public static List<MobileApplication> buildAppsFromPackageInfos(
+            @NonNull PackageManager packageManager,
+            @NonNull Map<PackageInfo, ResolveInfo> packageInfos) {
+        Objects.requireNonNull(packageManager);
+        Objects.requireNonNull(packageInfos);
+
+        List<MobileApplication> mobileApplications = new ArrayList<>();
+        for (Map.Entry<PackageInfo, ResolveInfo> entry : packageInfos.entrySet()) {
+            MobileApplication mobileApplication =
+                    createMobileApplication(packageManager, entry.getKey(), entry.getValue());
+            if (mobileApplication != null && !mobileApplication.getDisplayName().isEmpty()) {
+                mobileApplications.add(mobileApplication);
+            }
+        }
+        return mobileApplications;
+    }
+
+    /** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found */
+    @Nullable
+    public static byte[] getCertificate(@NonNull PackageInfo packageInfo) {
+        Objects.requireNonNull(packageInfo);
+        if (packageInfo.signingInfo == null) {
+            if (LogUtil.DEBUG) {
+                Log.d(TAG, "Signing info not found for package: " + packageInfo.packageName);
+            }
+            return null;
+        }
+        MessageDigest md;
+        try {
+            md = MessageDigest.getInstance("SHA256");
+        } catch (NoSuchAlgorithmException e) {
+            return null;
+        }
+        Signature[] signatures = packageInfo.signingInfo.getSigningCertificateHistory();
+        if (signatures == null || signatures.length == 0) {
+            return null;
+        }
+        md.update(signatures[0].toByteArray());
+        return md.digest();
+    }
+
+    /**
+     * Uses PackageManager to supplement packageInfos with an application display name and icon uri.
+     *
+     * @return a MobileApplication representing the packageInfo, null if finding the signing
+     *     certificate fails.
+     */
+    @Nullable
+    private static MobileApplication createMobileApplication(
+            @NonNull PackageManager packageManager,
+            @NonNull PackageInfo packageInfo,
+            @NonNull ResolveInfo resolveInfo) {
+        Objects.requireNonNull(packageManager);
+        Objects.requireNonNull(packageInfo);
+        Objects.requireNonNull(resolveInfo);
+
+        String applicationDisplayName = resolveInfo.loadLabel(packageManager).toString();
+        if (TextUtils.isEmpty(applicationDisplayName)) {
+            applicationDisplayName = packageInfo.applicationInfo.className;
+        }
+
+        String iconUri = getActivityIconUriString(packageManager, resolveInfo.activityInfo);
+
+        byte[] certificate = getCertificate(packageInfo);
+        if (certificate == null) {
+            return null;
+        }
+
+        MobileApplication.Builder builder =
+                new MobileApplication.Builder(packageInfo.packageName, certificate)
+                        .setDisplayName(applicationDisplayName)
+                        // TODO(b/275592563): Populate with nicknames from various sources
+                        .setCreationTimestampMillis(packageInfo.firstInstallTime)
+                        .setUpdatedTimestampMs(packageInfo.lastUpdateTime);
+
+        String applicationLabel =
+                packageManager.getApplicationLabel(packageInfo.applicationInfo).toString();
+        if (!applicationDisplayName.equals(applicationLabel)) {
+            // This can be different from applicationDisplayName, and should be indexed
+            builder.setAlternateNames(applicationLabel);
+        }
+
+        if (iconUri != null) {
+            builder.setIconUri(iconUri);
+        }
+
+        if (resolveInfo.activityInfo.name != null) {
+            builder.setClassName(resolveInfo.activityInfo.name);
+        }
+        return builder.build();
+    }
+}
+
diff --git a/service/java/com/android/server/appsearch/appsindexer/FrameworkAppsIndexerConfig.java b/service/java/com/android/server/appsearch/appsindexer/FrameworkAppsIndexerConfig.java
new file mode 100644
index 0000000..d7c0b98
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/FrameworkAppsIndexerConfig.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.provider.DeviceConfig;
+
+/**
+ * Implementation of {@link AppsIndexerConfig} using {@link DeviceConfig}.
+ *
+ * <p>It contains all the keys for flags related to Apps Indexer.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @hide
+ */
+public class FrameworkAppsIndexerConfig implements AppsIndexerConfig {
+    static final String KEY_APPS_INDEXER_ENABLED = "apps_indexer_enabled";
+    static final String KEY_APPS_UPDATE_INTERVAL_MILLIS = "apps_update_interval_millis";
+
+    @Override
+    public boolean isAppsIndexerEnabled() {
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_APPS_INDEXER_ENABLED,
+                DEFAULT_APPS_INDEXER_ENABLED);
+    }
+
+    @Override
+    public long getAppsMaintenanceUpdateIntervalMillis() {
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_APPS_UPDATE_INTERVAL_MILLIS,
+                DEFAULT_APPS_UPDATE_INTERVAL_MILLIS);
+    }
+}
+
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchBase.java b/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchBase.java
new file mode 100644
index 0000000..2c38d7d
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchBase.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.BatchResultCallback;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/** Contains common methods for converting async methods to sync */
+public class SyncAppSearchBase {
+    protected final Executor mExecutor;
+
+    public SyncAppSearchBase(@NonNull Executor executor) {
+        mExecutor = Objects.requireNonNull(executor);
+    }
+
+    protected <T> T executeAppSearchResultOperation(
+            Consumer<Consumer<AppSearchResult<T>>> operation) throws AppSearchException {
+        final CompletableFuture<AppSearchResult<T>> futureResult = new CompletableFuture<>();
+
+        mExecutor.execute(
+                () -> {
+                    operation.accept(futureResult::complete);
+                });
+
+        try {
+            // TODO(b/275592563): Change to get timeout value from config
+            AppSearchResult<T> result = futureResult.get();
+
+            if (!result.isSuccess()) {
+                throw new AppSearchException(result.getResultCode(), result.getErrorMessage());
+            }
+
+            return result.getResultValue();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR, "Operation was interrupted.", e);
+        } catch (ExecutionException e) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_UNKNOWN_ERROR,
+                    "Error executing operation.",
+                    e.getCause());
+        }
+    }
+
+    protected <T, V> AppSearchBatchResult<T, V> executeAppSearchBatchResultOperation(
+            Consumer<BatchResultCallback<T, V>> operation) throws AppSearchException {
+        final CompletableFuture<AppSearchBatchResult<T, V>> futureResult =
+                new CompletableFuture<>();
+
+        mExecutor.execute(
+                () ->
+                        operation.accept(
+                                new BatchResultCallback<>() {
+                                    @Override
+                                    public void onResult(
+                                            @NonNull AppSearchBatchResult<T, V> value) {
+                                        futureResult.complete(value);
+                                    }
+
+                                    @Override
+                                    public void onSystemError(@Nullable Throwable throwable) {
+                                        futureResult.completeExceptionally(throwable);
+                                    }
+                                }));
+
+        try {
+            // TODO(b/275592563): Change to get timeout value from config
+            return futureResult.get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_INTERNAL_ERROR, "Operation was interrupted.", e);
+        } catch (ExecutionException e) {
+            throw new AppSearchException(
+                    AppSearchResult.RESULT_UNKNOWN_ERROR,
+                    "Error executing operation.",
+                    e.getCause());
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchSession.java b/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchSession.java
new file mode 100644
index 0000000..38e649d
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchSession.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import java.io.Closeable;
+
+/**
+ * A synchronous wrapper around {@link AppSearchSession}. This allows us to perform operations in
+ * AppSearch without needing to handle async calls.
+ *
+ * @see AppSearchSession
+ */
+public interface SyncAppSearchSession extends Closeable {
+    /**
+     * Synchronously sets an {@link AppSearchSchema}.
+     *
+     * @see AppSearchSession#setSchema
+     */
+    @NonNull
+    SetSchemaResponse setSchema(@NonNull SetSchemaRequest setSchemaRequest)
+            throws AppSearchException;
+
+    /**
+     * Synchronously inserts documents into AppSearch.
+     *
+     * @see AppSearchSession#put
+     */
+    @NonNull
+    AppSearchBatchResult<String, Void> put(@NonNull PutDocumentsRequest request)
+            throws AppSearchException;
+
+    /**
+     * Returns a synchronous version of {@link SearchResults}.
+     *
+     * <p>While the underlying method is not asynchronous, this method allows for convenience while
+     * synchronously searching AppSearch.
+     *
+     * @see AppSearchSession#search
+     */
+    @NonNull
+    SyncSearchResults search(@NonNull String query, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Closes the session.
+     *
+     * @see AppSearchSession#close
+     */
+    @Override
+    void close();
+
+    // TODO(b/275592563): Bring in additional methods such as getByDocumentId as needed
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchSessionImpl.java b/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchSessionImpl.java
new file mode 100644
index 0000000..458da02
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncAppSearchSessionImpl.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/** SyncAppSearchSessionImpl methods are a super set of SyncGlobalSearchSessionImpl methods. */
+public class SyncAppSearchSessionImpl extends SyncAppSearchBase implements SyncAppSearchSession {
+    private final AppSearchSession mSession;
+
+    public SyncAppSearchSessionImpl(
+            @NonNull AppSearchManager appSearchManager,
+            @NonNull AppSearchManager.SearchContext searchContext,
+            @NonNull Executor executor)
+            throws AppSearchException {
+        super(executor);
+        Objects.requireNonNull(appSearchManager);
+        Objects.requireNonNull(searchContext);
+        Objects.requireNonNull(executor);
+        mSession =
+                executeAppSearchResultOperation(
+                        resultHandler ->
+                                appSearchManager.createSearchSession(
+                                        searchContext, executor, resultHandler));
+    }
+
+    // Not actually asynchronous but added for convenience
+    @Override
+    @NonNull
+    public SyncSearchResults search(@NonNull String query, @NonNull SearchSpec searchSpec) {
+        Objects.requireNonNull(query);
+        Objects.requireNonNull(searchSpec);
+        return new SyncSearchResultsImpl(mSession.search(query, searchSpec), mExecutor);
+    }
+
+    @Override
+    @NonNull
+    public SetSchemaResponse setSchema(@NonNull SetSchemaRequest setSchemaRequest)
+            throws AppSearchException {
+        Objects.requireNonNull(setSchemaRequest);
+        return executeAppSearchResultOperation(
+                resultHandler ->
+                        mSession.setSchema(setSchemaRequest, mExecutor, mExecutor, resultHandler));
+    }
+
+    // Put involves an AppSearchBatchResult, so it can't be simplified through
+    // executeAppSearchResultOperation. Instead we use executeAppSearchBatchResultOperation.
+    @Override
+    @NonNull
+    public AppSearchBatchResult<String, Void> put(@NonNull PutDocumentsRequest request)
+            throws AppSearchException {
+        Objects.requireNonNull(request);
+        return executeAppSearchBatchResultOperation(
+                resultHandler -> mSession.put(request, mExecutor, resultHandler));
+    }
+
+    // Also not asynchronous but it's necessary to be able to close the session
+    @Override
+    public void close() {
+        mSession.close();
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncGlobalSearchSession.java b/service/java/com/android/server/appsearch/appsindexer/SyncGlobalSearchSession.java
new file mode 100644
index 0000000..ca19417
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncGlobalSearchSession.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.SearchSpec;
+
+import java.io.Closeable;
+
+/**
+ * A synchronous wrapper around {@link GlobalSearchSession}. This allows us to call globalSearch
+ * synchronously.
+ *
+ * @see GlobalSearchSession
+ */
+public interface SyncGlobalSearchSession extends Closeable {
+    /**
+     * Returns a synchronous version of {@link SearchResults}.
+     *
+     * <p>While the underlying method is not asynchronous, this method allows for convenience while
+     * synchronously searching globally.
+     *
+     * @see GlobalSearchSession#search
+     */
+    @NonNull
+    SyncSearchResults search(@NonNull String query, @NonNull SearchSpec searchSpec);
+
+    /**
+     * Closes the global session.
+     *
+     * @see GlobalSearchSession#close
+     */
+    @Override
+    void close();
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncGlobalSearchSessionImpl.java b/service/java/com/android/server/appsearch/appsindexer/SyncGlobalSearchSessionImpl.java
new file mode 100644
index 0000000..8021cef
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncGlobalSearchSessionImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+public class SyncGlobalSearchSessionImpl extends SyncAppSearchBase
+        implements SyncGlobalSearchSession {
+
+    private final GlobalSearchSession mGlobalSession;
+
+    public SyncGlobalSearchSessionImpl(
+            @NonNull AppSearchManager appSearchManager, @NonNull Executor executor)
+            throws AppSearchException {
+        super(executor);
+        Objects.requireNonNull(appSearchManager);
+        Objects.requireNonNull(executor);
+
+        mGlobalSession =
+                executeAppSearchResultOperation(
+                        resultHandler ->
+                                appSearchManager.createGlobalSearchSession(
+                                        executor, resultHandler));
+    }
+
+    // Not actually asynchronous but added for convenience
+    @Override
+    @NonNull
+    public SyncSearchResults search(@NonNull String query, @NonNull SearchSpec searchSpec) {
+        Objects.requireNonNull(query);
+        Objects.requireNonNull(searchSpec);
+        return new SyncSearchResultsImpl(mGlobalSession.search(query, searchSpec), mExecutor);
+    }
+
+    // Also not asynchronous but it's necessary to be able to close the session
+    @Override
+    public void close() {
+        mGlobalSession.close();
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncSearchResults.java b/service/java/com/android/server/appsearch/appsindexer/SyncSearchResults.java
new file mode 100644
index 0000000..db26ef3
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncSearchResults.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import java.util.List;
+
+/**
+ * A synchronous wrapper for {@link SearchResults}. This allows us to call getNextPage in a loop if
+ * needed.
+ *
+ * @see SearchResults
+ */
+public interface SyncSearchResults {
+    /**
+     * Synchronously returns a list of {@link SearchResult}s.
+     *
+     * @see SearchResults#getNextPage
+     */
+    @NonNull
+    List<SearchResult> getNextPage() throws AppSearchException;
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/SyncSearchResultsImpl.java b/service/java/com/android/server/appsearch/appsindexer/SyncSearchResultsImpl.java
new file mode 100644
index 0000000..6af2efd
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/SyncSearchResultsImpl.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
+import android.app.appsearch.exceptions.AppSearchException;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+public class SyncSearchResultsImpl extends SyncAppSearchBase implements SyncSearchResults {
+    private final SearchResults mSearchResults;
+
+    public SyncSearchResultsImpl(SearchResults searchResults, @NonNull Executor executor) {
+        super(executor);
+        mSearchResults = Objects.requireNonNull(searchResults);
+    }
+
+    @NonNull
+    @Override
+    public List<SearchResult> getNextPage() throws AppSearchException {
+        return executeAppSearchResultOperation(
+                resultHandler -> mSearchResults.getNextPage(mExecutor, resultHandler));
+    }
+}
diff --git a/service/java/com/android/server/appsearch/appsindexer/appsearchtypes/MobileApplication.java b/service/java/com/android/server/appsearch/appsindexer/appsearchtypes/MobileApplication.java
new file mode 100644
index 0000000..3e1d312
--- /dev/null
+++ b/service/java/com/android/server/appsearch/appsindexer/appsearchtypes/MobileApplication.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer.appsearchtypes;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.GenericDocument;
+import android.net.Uri;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+/**
+ * Represents a installed app to enable searching using name, nicknames, and package name>
+ *
+ * @hide
+ */
+public class MobileApplication extends GenericDocument {
+
+    private static final String TAG = "AppSearchMobileApplication";
+
+    public static final String SCHEMA_TYPE = "builtin:MobileApplication";
+    public static final String APPS_NAMESPACE = "apps";
+
+    public static final String APP_PROPERTY_PACKAGE_NAME = "packageName";
+    public static final String APP_PROPERTY_DISPLAY_NAME = "displayName";
+    public static final String APP_PROPERTY_ALTERNATE_NAMES = "alternateNames";
+    public static final String APP_PROPERTY_ICON_URI = "iconUri";
+    public static final String APP_PROPERTY_SHA256_CERTIFICATE = "sha256Certificate";
+    public static final String APP_PROPERTY_UPDATED_TIMESTAMP = "updatedTimestamp";
+    public static final String APP_PROPERTY_CLASS_NAME = "className";
+
+    /** Returns a per-app schema name. */
+    @VisibleForTesting
+    public static String getSchemaNameForPackage(@NonNull String pkg) {
+        return SCHEMA_TYPE + "-" + Objects.requireNonNull(pkg);
+    }
+
+    /**
+     * Returns a MobileApplication {@link AppSearchSchema} for the a package.
+     *
+     * <p>This is necessary as the base schema and the per-app schemas share all the same
+     * properties. However, we cannot easily modify the base schema to create a per-app schema. So
+     * instead, to create the base schema we call this method with a blank AppSearchSchema with a
+     * schema type of SCHEMA_TYPE. For per-app schemas, we set the schema type to a per-app schema
+     * type, add a parent type of SCHEMA_TYPE, then add the properties.
+     *
+     * @param packageName The package name to create a schema for. Will create the base schema if
+     *     called with null.
+     */
+    @NonNull
+    public static AppSearchSchema createMobileApplicationSchemaForPackage(
+            @NonNull String packageName) {
+        Objects.requireNonNull(packageName);
+        return new AppSearchSchema.Builder(getSchemaNameForPackage(packageName))
+                // It's possible the user knows the package name, or wants to search for all apps
+                // from a certain developer. They could search for "com.developer.*".
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(APP_PROPERTY_PACKAGE_NAME)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig
+                                                .TOKENIZER_TYPE_VERBATIM)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(APP_PROPERTY_DISPLAY_NAME)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(
+                                        APP_PROPERTY_ALTERNATE_NAMES)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(APP_PROPERTY_ICON_URI)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.BytesPropertyConfig.Builder(
+                                        APP_PROPERTY_SHA256_CERTIFICATE)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.LongPropertyConfig.Builder(
+                                        APP_PROPERTY_UPDATED_TIMESTAMP)
+                                .setIndexingType(
+                                        AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(APP_PROPERTY_CLASS_NAME)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                .build();
+    }
+
+    /** Constructs a {@link MobileApplication}. */
+    @VisibleForTesting
+    public MobileApplication(@NonNull GenericDocument document) {
+        super(document);
+    }
+
+    /**
+     * Returns the package name this {@link MobileApplication} represents. For example,
+     * "com.android.vending".
+     */
+    @NonNull
+    public String getPackageName() {
+        return getId();
+    }
+
+    /**
+     * Returns the display name of the app. This is indexed. This is what is displayed in the
+     * launcher. This might look like "Play Store".
+     */
+    @Nullable
+    public String getDisplayName() {
+        return getPropertyString(APP_PROPERTY_DISPLAY_NAME);
+    }
+
+    /**
+     * Returns alternative names of the application. These are indexed. For example, you might have
+     * the alternative name "pay" for a wallet app.
+     */
+    @Nullable
+    public String[] getAlternateNames() {
+        return getPropertyStringArray(APP_PROPERTY_ALTERNATE_NAMES);
+    }
+
+    /**
+     * Returns the full name of the resource identifier of the app icon, which can be used for
+     * displaying results. The Uri could be
+     * "android.resource://com.example.vending/drawable/2131230871", for example.
+     */
+    @Nullable
+    public Uri getIconUri() {
+        String uriStr = getPropertyString(APP_PROPERTY_ICON_URI);
+        if (uriStr == null) {
+            return null;
+        }
+        try {
+            return Uri.parse(uriStr);
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    /** Returns the SHA-256 certificate of the application. */
+    @NonNull
+    public byte[] getSha256Certificate() {
+        return getPropertyBytes(APP_PROPERTY_SHA256_CERTIFICATE);
+    }
+
+    /** Returns the last time the app was installed or updated on the device. */
+    @CurrentTimeMillisLong
+    public long getUpdatedTimestamp() {
+        return getPropertyLong(APP_PROPERTY_UPDATED_TIMESTAMP);
+    }
+
+    /**
+     * Returns the fully qualified name of the Application class for this mobile app. This would
+     * look something like "com.android.vending.SearchActivity". Combined with the package name, a
+     * launch intent can be created with <code>
+     *     Intent launcher = new Intent(Intent.ACTION_MAIN);
+     *     launcher.setComponent(new ComponentName(app.getPackageName(), app.getClassName()));
+     *     launcher.setPackage(app.getPackageName());
+     *     launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+     *     launcher.addCategory(Intent.CATEGORY_LAUNCHER);
+     *     appListFragment.getActivity().startActivity(launcher);
+     *  </code>
+     */
+    @Nullable
+    public String getClassName() {
+        return getPropertyString(APP_PROPERTY_CLASS_NAME);
+    }
+
+    public static final class Builder extends GenericDocument.Builder<Builder> {
+        public Builder(@NonNull String packageName, @NonNull byte[] sha256Certificate) {
+            // Changing the schema type dynamically so that we can use separate schemas
+            super(
+                    APPS_NAMESPACE,
+                    Objects.requireNonNull(packageName),
+                    getSchemaNameForPackage(packageName));
+            setPropertyString(APP_PROPERTY_PACKAGE_NAME, packageName);
+            setPropertyBytes(
+                    APP_PROPERTY_SHA256_CERTIFICATE, Objects.requireNonNull(sha256Certificate));
+        }
+
+        /** Sets the display name. */
+        @NonNull
+        public Builder setDisplayName(@NonNull String displayName) {
+            setPropertyString(APP_PROPERTY_DISPLAY_NAME, Objects.requireNonNull(displayName));
+            return this;
+        }
+
+        /** Sets the alternate names. An empty array will erase the list. */
+        @NonNull
+        public Builder setAlternateNames(@NonNull String... alternateNames) {
+            setPropertyString(APP_PROPERTY_ALTERNATE_NAMES, Objects.requireNonNull(alternateNames));
+            return this;
+        }
+
+        /** Sets the icon uri. */
+        @NonNull
+        public Builder setIconUri(@NonNull String iconUri) {
+            setPropertyString(APP_PROPERTY_ICON_URI, Objects.requireNonNull(iconUri));
+            return this;
+        }
+
+        @NonNull
+        public Builder setUpdatedTimestampMs(@CurrentTimeMillisLong long updatedTimestampMs) {
+            setPropertyLong(APP_PROPERTY_UPDATED_TIMESTAMP, updatedTimestampMs);
+            return this;
+        }
+
+        /** Sets the class name. */
+        @NonNull
+        public Builder setClassName(@NonNull String className) {
+            setPropertyString(APP_PROPERTY_CLASS_NAME, Objects.requireNonNull(className));
+            return this;
+        }
+
+        @NonNull
+        public MobileApplication build() {
+            return new MobileApplication(super.build());
+        }
+    }
+}
+
diff --git a/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java b/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
index 3cdecc3..fbb9054 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/AppSearchHelper.java
@@ -96,14 +96,16 @@
             @NonNull Context context,
             @NonNull Executor executor,
             @NonNull ContactsIndexerConfig contactsIndexerConfig) {
-        AppSearchHelper appSearchHelper = new AppSearchHelper(context, executor,
-                contactsIndexerConfig);
+        AppSearchHelper appSearchHelper =
+                new AppSearchHelper(context, executor, contactsIndexerConfig);
         appSearchHelper.initializeAsync();
         return appSearchHelper;
     }
 
     @VisibleForTesting
-    AppSearchHelper(@NonNull Context context, @NonNull Executor executor,
+    AppSearchHelper(
+            @NonNull Context context,
+            @NonNull Executor executor,
             @NonNull ContactsIndexerConfig contactsIndexerConfig) {
         mContext = Objects.requireNonNull(context);
         mExecutor = Objects.requireNonNull(executor);
@@ -113,8 +115,8 @@
     /**
      * Initializes {@link AppSearchHelper} asynchronously.
      *
-     * <p>Chains {@link CompletableFuture}s to create an {@link AppSearchSession} and
-     * set builtin:Person schema.
+     * <p>Chains {@link CompletableFuture}s to create an {@link AppSearchSession} and set
+     * builtin:Person schema.
      */
     private void initializeAsync() {
         AppSearchManager appSearchManager = mContext.getSystemService(AppSearchManager.class);
@@ -125,30 +127,47 @@
 
         CompletableFuture<AppSearchSession> createSessionFuture =
                 createAppSearchSessionAsync(appSearchManager);
-        mAppSearchSessionFuture = createSessionFuture.thenCompose(appSearchSession -> {
-            // set the schema with forceOverride false first. And if it fails, we will set the
-            // schema with forceOverride true. This way, we know when the data is wiped due to an
-            // incompatible schema change, which is the main cause for the 1st setSchema to fail.
-            return setPersonSchemaAsync(appSearchSession, /*forceOverride=*/ false)
-                    .handle((x, e) -> {
-                        boolean firstSetSchemaFailed = false;
-                        if (e != null) {
-                            Log.w(TAG, "Error while setting schema with forceOverride false.", e);
-                            firstSetSchemaFailed = true;
-                        }
-                        return firstSetSchemaFailed;
-                    }).thenCompose(firstSetSchemaFailed -> {
-                        mDataLikelyWipedDuringInitFuture.complete(firstSetSchemaFailed);
-                        if (firstSetSchemaFailed) {
-                            // Try setSchema with forceOverride true.
-                            // If it succeeds, we know the data is likely to be wiped due to an
-                            // incompatible schema change.
-                            // If if fails, we don't know the state of that corpus in AppSearch.
-                            return setPersonSchemaAsync(appSearchSession, /*forceOverride=*/ true);
-                        }
-                        return CompletableFuture.completedFuture(appSearchSession);
-                    });
-        });
+        mAppSearchSessionFuture =
+                createSessionFuture.thenCompose(
+                        appSearchSession -> {
+                            // set the schema with forceOverride false first. And if it fails, we
+                            // will set the schema with forceOverride true. This way, we know when
+                            // the data is wiped due to an incompatible schema change, which is the
+                            // main cause for the 1st setSchema to fail.
+                            return setPersonSchemaAsync(
+                                            appSearchSession, /* forceOverride= */ false)
+                                    .handle(
+                                            (x, e) -> {
+                                                boolean firstSetSchemaFailed = false;
+                                                if (e != null) {
+                                                    Log.w(
+                                                            TAG,
+                                                            "Error while setting schema with"
+                                                                    + " forceOverride false.",
+                                                            e);
+                                                    firstSetSchemaFailed = true;
+                                                }
+                                                return firstSetSchemaFailed;
+                                            })
+                                    .thenCompose(
+                                            firstSetSchemaFailed -> {
+                                                mDataLikelyWipedDuringInitFuture.complete(
+                                                        firstSetSchemaFailed);
+                                                if (firstSetSchemaFailed) {
+                                                    // Try setSchema with forceOverride true.
+                                                    // If it succeeds, we know the data is likely to
+                                                    // be wiped due to an
+                                                    // incompatible schema change.
+                                                    // If if fails, we don't know the state of that
+                                                    // corpus in AppSearch.
+                                                    return setPersonSchemaAsync(
+                                                            appSearchSession,
+                                                            /* forceOverride= */ true);
+                                                }
+                                                return CompletableFuture.completedFuture(
+                                                        appSearchSession);
+                                            });
+                        });
     }
 
     /**
@@ -164,16 +183,24 @@
         CompletableFuture<AppSearchSession> future = new CompletableFuture<>();
         final AppSearchManager.SearchContext searchContext =
                 new AppSearchManager.SearchContext.Builder(DATABASE_NAME).build();
-        appSearchManager.createSearchSession(searchContext, mExecutor, result -> {
-            if (result.isSuccess()) {
-                future.complete(result.getResultValue());
-            } else {
-                Log.e(TAG, "Failed to create an AppSearchSession - code: " + result.getResultCode()
-                        + " errorMessage: " + result.getErrorMessage());
-                future.completeExceptionally(
-                        new AppSearchException(result.getResultCode(), result.getErrorMessage()));
-            }
-        });
+        appSearchManager.createSearchSession(
+                searchContext,
+                mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.complete(result.getResultValue());
+                    } else {
+                        Log.e(
+                                TAG,
+                                "Failed to create an AppSearchSession - code: "
+                                        + result.getResultCode()
+                                        + " errorMessage: "
+                                        + result.getErrorMessage());
+                        future.completeExceptionally(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
 
         return future;
     }
@@ -184,7 +211,7 @@
      * <p>It returns {@link CompletableFuture} so caller can wait for valid schemas set, which must
      * be done before ContactsIndexer starts handling CP2 changes.
      *
-     * @param session       {@link AppSearchSession} created before.
+     * @param session {@link AppSearchSession} created before.
      * @param forceOverride whether the incompatible schemas should be overridden.
      */
     @NonNull
@@ -193,30 +220,42 @@
         Objects.requireNonNull(session);
 
         CompletableFuture<AppSearchSession> future = new CompletableFuture<>();
-        SetSchemaRequest.Builder schemaBuilder = new SetSchemaRequest.Builder()
-                .addSchemas(ContactPoint.SCHEMA, Person.getSchema(mContactsIndexerConfig))
-                .addRequiredPermissionsForSchemaTypeVisibility(Person.SCHEMA_TYPE,
-                        Collections.singleton(SetSchemaRequest.READ_CONTACTS))
-                // Adds a permission set that allows the Person schema to be read by an enterprise
-                // session. The set contains ENTERPRISE_ACCESS which makes it visible to enterprise
-                // sessions and unsatisfiable for regular sessions. The set also requires the caller
-                // to have regular read contacts access and managed profile contacts access.
-                .addRequiredPermissionsForSchemaTypeVisibility(Person.SCHEMA_TYPE,
-                        new ArraySet<>(Arrays.asList(
-                                SetSchemaRequest.ENTERPRISE_ACCESS,
-                                SetSchemaRequest.READ_CONTACTS,
-                                SetSchemaRequest.MANAGED_PROFILE_CONTACTS_ACCESS
-                        )))
-                .setForceOverride(forceOverride);
-        session.setSchema(schemaBuilder.build(), mExecutor, mExecutor,
+        SetSchemaRequest.Builder schemaBuilder =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(ContactPoint.SCHEMA, Person.getSchema(mContactsIndexerConfig))
+                        .addRequiredPermissionsForSchemaTypeVisibility(
+                                Person.SCHEMA_TYPE,
+                                Collections.singleton(SetSchemaRequest.READ_CONTACTS))
+                        // Adds a permission set that allows the Person schema to be read by an
+                        // enterprise session. The set contains ENTERPRISE_ACCESS which makes it
+                        // visible to enterprise sessions and unsatisfiable for regular sessions.
+                        // The set also requires the caller to have regular read contacts access and
+                        // managed profile contacts access.
+                        .addRequiredPermissionsForSchemaTypeVisibility(
+                                Person.SCHEMA_TYPE,
+                                new ArraySet<>(
+                                        Arrays.asList(
+                                                SetSchemaRequest.ENTERPRISE_ACCESS,
+                                                SetSchemaRequest.READ_CONTACTS,
+                                                SetSchemaRequest.MANAGED_PROFILE_CONTACTS_ACCESS)))
+                        .setForceOverride(forceOverride);
+        session.setSchema(
+                schemaBuilder.build(),
+                mExecutor,
+                mExecutor,
                 result -> {
                     if (result.isSuccess()) {
                         future.complete(session);
                     } else {
-                        Log.e(TAG, "SetSchema failed: code " + result.getResultCode() + " message:"
-                                + result.getErrorMessage());
-                        future.completeExceptionally(new AppSearchException(result.getResultCode(),
-                                result.getErrorMessage()));
+                        Log.e(
+                                TAG,
+                                "SetSchema failed: code "
+                                        + result.getResultCode()
+                                        + " message:"
+                                        + result.getErrorMessage());
+                        future.completeExceptionally(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
                     }
                 });
         return future;
@@ -243,8 +282,8 @@
      * happens:
      * <li>If the value is {@code false}, we are sure there is NO data loss.
      * <li>If the value is {@code true}, it is very likely the data loss happens, or the whole
-     * initialization fails and the data state is unknown. Callers need to query AppSearch to
-     * confirm.
+     *     initialization fails and the data state is unknown. Callers need to query AppSearch to
+     *     confirm.
      */
     @NonNull
     public CompletableFuture<Boolean> isDataLikelyWipedDuringInitAsync() {
@@ -256,20 +295,18 @@
     /**
      * Indexes contacts into AppSearch
      *
-     * @param contacts    a collection of contacts. AppSearch batch put will be used to send the
-     *                    documents over in one call. So the size of this collection can't be too
-     *                    big, otherwise binder {@link android.os.TransactionTooLargeException} will
-     *                    be thrown.
+     * @param contacts a collection of contacts. AppSearch batch put will be used to send the
+     *     documents over in one call. So the size of this collection can't be too big, otherwise
+     *     binder {@link android.os.TransactionTooLargeException} will be thrown.
      * @param updateStats to hold the counters for the update.
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors. When true, the
-     *                                  returned future completes normally even when contacts have
-     *                                  failed to be added. AppSearchResult#RESULT_OUT_OF_SPACE
-     *                                  failures are an exception to this however and will still
-     *                                  complete exceptionally.
+     *     should continue after encountering errors. When true, the returned future completes
+     *     normally even when contacts have failed to be added. AppSearchResult#RESULT_OUT_OF_SPACE
+     *     failures are an exception to this however and will still complete exceptionally.
      */
     @NonNull
-    public CompletableFuture<Void> indexContactsAsync(@NonNull Collection<Person> contacts,
+    public CompletableFuture<Void> indexContactsAsync(
+            @NonNull Collection<Person> contacts,
             @NonNull ContactsUpdateStats updateStats,
             boolean shouldKeepUpdatingOnError) {
         Objects.requireNonNull(contacts);
@@ -278,87 +315,102 @@
         if (LogUtil.DEBUG) {
             Log.v(TAG, "Indexing " + contacts.size() + " contacts into AppSearch");
         }
-        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
-                .addGenericDocuments(contacts)
-                .build();
-        return mAppSearchSessionFuture.thenCompose(appSearchSession -> {
-            CompletableFuture<Void> future = new CompletableFuture<>();
-            appSearchSession.put(request, mExecutor, new BatchResultCallback<>() {
-                @Override
-                public void onResult(AppSearchBatchResult<String, Void> result) {
-                    int numDocsSucceeded = result.getSuccesses().size();
-                    int numDocsFailed = result.getFailures().size();
-                    updateStats.mContactsUpdateSucceededCount += numDocsSucceeded;
-                    if (result.isSuccess()) {
-                        if (LogUtil.DEBUG) {
-                            Log.v(TAG, numDocsSucceeded
-                                    + " documents successfully added in AppSearch.");
-                        }
-                        future.complete(null);
-                    } else {
-                        Map<String, AppSearchResult<Void>> failures = result.getFailures();
-                        AppSearchResult<Void> firstFailure = null;
-                        for (AppSearchResult<Void> failure : failures.values()) {
-                            int errorCode = failure.getResultCode();
-                            if (firstFailure == null) {
-                                if (shouldKeepUpdatingOnError) {
-                                    // Still complete exceptionally (and abort further indexing) if
-                                    // AppSearchResult#RESULT_OUT_OF_SPACE
-                                    if (errorCode == AppSearchResult.RESULT_OUT_OF_SPACE) {
-                                        firstFailure = failure;
+        PutDocumentsRequest request =
+                new PutDocumentsRequest.Builder().addGenericDocuments(contacts).build();
+        return mAppSearchSessionFuture.thenCompose(
+                appSearchSession -> {
+                    CompletableFuture<Void> future = new CompletableFuture<>();
+                    appSearchSession.put(
+                            request,
+                            mExecutor,
+                            new BatchResultCallback<>() {
+                                @Override
+                                public void onResult(AppSearchBatchResult<String, Void> result) {
+                                    int numDocsSucceeded = result.getSuccesses().size();
+                                    int numDocsFailed = result.getFailures().size();
+                                    updateStats.mContactsUpdateSucceededCount += numDocsSucceeded;
+                                    if (result.isSuccess()) {
+                                        if (LogUtil.DEBUG) {
+                                            Log.v(
+                                                    TAG,
+                                                    numDocsSucceeded
+                                                            + " documents successfully added in"
+                                                            + " AppSearch.");
+                                        }
+                                        future.complete(null);
+                                    } else {
+                                        Map<String, AppSearchResult<Void>> failures =
+                                                result.getFailures();
+                                        AppSearchResult<Void> firstFailure = null;
+                                        for (AppSearchResult<Void> failure : failures.values()) {
+                                            int errorCode = failure.getResultCode();
+                                            if (firstFailure == null) {
+                                                if (shouldKeepUpdatingOnError) {
+                                                    // Still complete exceptionally (and abort
+                                                    // further indexing) if
+                                                    // AppSearchResult#RESULT_OUT_OF_SPACE
+                                                    if (errorCode
+                                                            == AppSearchResult
+                                                                    .RESULT_OUT_OF_SPACE) {
+                                                        firstFailure = failure;
+                                                    }
+                                                } else {
+                                                    firstFailure = failure;
+                                                }
+                                            }
+                                            updateStats.mUpdateStatuses.add(errorCode);
+                                        }
+                                        if (firstFailure == null) {
+                                            future.complete(null);
+                                        } else {
+                                            Log.w(
+                                                    TAG,
+                                                    numDocsFailed
+                                                            + " documents failed to be added in"
+                                                            + " AppSearch.");
+                                            future.completeExceptionally(
+                                                    new AppSearchException(
+                                                            firstFailure.getResultCode(),
+                                                            firstFailure.getErrorMessage()));
+                                        }
                                     }
-                                } else {
-                                    firstFailure = failure;
                                 }
-                            }
-                            updateStats.mUpdateStatuses.add(errorCode);
-                        }
-                        if (firstFailure == null) {
-                            future.complete(null);
-                        } else {
-                            Log.w(TAG,
-                                    numDocsFailed + " documents failed to be added in AppSearch.");
-                            future.completeExceptionally(
-                                    new AppSearchException(firstFailure.getResultCode(),
-                                            firstFailure.getErrorMessage()));
-                        }
-                    }
-                }
 
-                @Override
-                public void onSystemError(Throwable throwable) {
-                    Log.e(TAG, "Failed to add contacts", throwable);
-                    // Log a combined status code; ranges of the codes do not overlap 10100 + 0-99
-                    updateStats.mUpdateStatuses.add(
-                            ContactsUpdateStats.ERROR_CODE_APP_SEARCH_SYSTEM_ERROR
-                                    + AppSearchResult.throwableToFailedResult(
-                                    throwable).getResultCode());
-                    if (shouldKeepUpdatingOnError) {
-                        future.complete(null);
-                    } else {
-                        future.completeExceptionally(throwable);
-                    }
-                }
-            });
-            return future;
-        });
+                                @Override
+                                public void onSystemError(Throwable throwable) {
+                                    Log.e(TAG, "Failed to add contacts", throwable);
+                                    // Log a combined status code; ranges of the codes do not
+                                    // overlap 10100 + 0-99
+                                    updateStats.mUpdateStatuses.add(
+                                            ContactsUpdateStats.ERROR_CODE_APP_SEARCH_SYSTEM_ERROR
+                                                    + AppSearchResult.throwableToFailedResult(
+                                                                    throwable)
+                                                            .getResultCode());
+                                    if (shouldKeepUpdatingOnError) {
+                                        future.complete(null);
+                                    } else {
+                                        future.completeExceptionally(throwable);
+                                    }
+                                }
+                            });
+                    return future;
+                });
     }
 
     /**
      * Remove contacts from AppSearch
      *
-     * @param ids         a collection of contact ids. AppSearch batch remove will be used to send
-     *                    the ids over in one call. So the size of this collection can't be too
-     *                    big, otherwise binder {@link android.os.TransactionTooLargeException}
-     *                    will be thrown.
+     * @param ids a collection of contact ids. AppSearch batch remove will be used to send the ids
+     *     over in one call. So the size of this collection can't be too big, otherwise binder
+     *     {@link android.os.TransactionTooLargeException} will be thrown.
      * @param updateStats to hold the counters for the update.
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors. When enabled,
-     *                                  the returned future completes normally even when contacts
-     *                                  have failed to be removed.
+     *     should continue after encountering errors. When enabled, the returned future completes
+     *     normally even when contacts have failed to be removed.
      */
     @NonNull
-    public CompletableFuture<Void> removeContactsByIdAsync(@NonNull Collection<String> ids,
+    public CompletableFuture<Void> removeContactsByIdAsync(
+            @NonNull Collection<String> ids,
             @NonNull ContactsUpdateStats updateStats,
             boolean shouldKeepUpdatingOnError) {
         Objects.requireNonNull(ids);
@@ -367,108 +419,129 @@
         if (LogUtil.DEBUG) {
             Log.v(TAG, "Removing " + ids.size() + " contacts from AppSearch");
         }
-        RemoveByDocumentIdRequest request = new RemoveByDocumentIdRequest.Builder(NAMESPACE_NAME)
-                .addIds(ids)
-                .build();
-        return mAppSearchSessionFuture.thenCompose(appSearchSession -> {
-            CompletableFuture<Void> future = new CompletableFuture<>();
-            appSearchSession.remove(request, mExecutor, new BatchResultCallback<>() {
-                @Override
-                public void onResult(AppSearchBatchResult<String, Void> result) {
-                    int numSuccesses = result.getSuccesses().size();
-                    int numFailures = result.getFailures().size();
-                    int numNotFound = 0;
-                    updateStats.mContactsDeleteSucceededCount += numSuccesses;
-                    if (result.isSuccess()) {
-                        if (LogUtil.DEBUG) {
-                            Log.v(TAG, numSuccesses
-                                    + " documents successfully deleted from AppSearch.");
-                        }
-                        future.complete(null);
-                    } else {
-                        AppSearchResult<Void> firstFailure = null;
-                        for (AppSearchResult<Void> failedResult : result.getFailures().values()) {
-                            // Ignore failures if the error code is AppSearchResult#RESULT_NOT_FOUND
-                            // or if shouldKeepUpdatingOnError is true
-                            int errorCode = failedResult.getResultCode();
-                            if (errorCode == AppSearchResult.RESULT_NOT_FOUND) {
-                                numNotFound++;
-                            } else if (firstFailure == null
-                                    && !shouldKeepUpdatingOnError) {
-                                firstFailure = failedResult;
-                            }
-                            updateStats.mDeleteStatuses.add(errorCode);
-                        }
-                        updateStats.mContactsDeleteNotFoundCount += numNotFound;
-                        if (firstFailure == null) {
-                            future.complete(null);
-                        } else {
-                            Log.w(TAG,
-                                    "Failed to delete " + numFailures + " contacts from AppSearch");
-                            future.completeExceptionally(
-                                    new AppSearchException(firstFailure.getResultCode(),
-                                            firstFailure.getErrorMessage()));
-                        }
-                    }
-                }
+        RemoveByDocumentIdRequest request =
+                new RemoveByDocumentIdRequest.Builder(NAMESPACE_NAME).addIds(ids).build();
+        return mAppSearchSessionFuture.thenCompose(
+                appSearchSession -> {
+                    CompletableFuture<Void> future = new CompletableFuture<>();
+                    appSearchSession.remove(
+                            request,
+                            mExecutor,
+                            new BatchResultCallback<>() {
+                                @Override
+                                public void onResult(AppSearchBatchResult<String, Void> result) {
+                                    int numSuccesses = result.getSuccesses().size();
+                                    int numFailures = result.getFailures().size();
+                                    int numNotFound = 0;
+                                    updateStats.mContactsDeleteSucceededCount += numSuccesses;
+                                    if (result.isSuccess()) {
+                                        if (LogUtil.DEBUG) {
+                                            Log.v(
+                                                    TAG,
+                                                    numSuccesses
+                                                            + " documents successfully deleted from"
+                                                            + " AppSearch.");
+                                        }
+                                        future.complete(null);
+                                    } else {
+                                        AppSearchResult<Void> firstFailure = null;
+                                        for (AppSearchResult<Void> failedResult :
+                                                result.getFailures().values()) {
+                                            // Ignore failures if the error code is
+                                            // AppSearchResult#RESULT_NOT_FOUND
+                                            // or if shouldKeepUpdatingOnError is true
+                                            int errorCode = failedResult.getResultCode();
+                                            if (errorCode == AppSearchResult.RESULT_NOT_FOUND) {
+                                                numNotFound++;
+                                            } else if (firstFailure == null
+                                                    && !shouldKeepUpdatingOnError) {
+                                                firstFailure = failedResult;
+                                            }
+                                            updateStats.mDeleteStatuses.add(errorCode);
+                                        }
+                                        updateStats.mContactsDeleteNotFoundCount += numNotFound;
+                                        if (firstFailure == null) {
+                                            future.complete(null);
+                                        } else {
+                                            Log.w(
+                                                    TAG,
+                                                    "Failed to delete "
+                                                            + numFailures
+                                                            + " contacts from AppSearch");
+                                            future.completeExceptionally(
+                                                    new AppSearchException(
+                                                            firstFailure.getResultCode(),
+                                                            firstFailure.getErrorMessage()));
+                                        }
+                                    }
+                                }
 
-                @Override
-                public void onSystemError(Throwable throwable) {
-                    Log.e(TAG, "Failed to delete contacts", throwable);
-                    // Log a combined status code; ranges of the codes do not overlap 10100 + 0-99
-                    updateStats.mDeleteStatuses.add(
-                            ContactsUpdateStats.ERROR_CODE_APP_SEARCH_SYSTEM_ERROR
-                                    + AppSearchResult.throwableToFailedResult(
-                                    throwable).getResultCode());
-                    if (shouldKeepUpdatingOnError) {
-                        future.complete(null);
-                    } else {
-                        future.completeExceptionally(throwable);
-                    }
-                }
-            });
-            return future;
-        });
+                                @Override
+                                public void onSystemError(Throwable throwable) {
+                                    Log.e(TAG, "Failed to delete contacts", throwable);
+                                    // Log a combined status code; ranges of the codes do not
+                                    // overlap 10100 + 0-99
+                                    updateStats.mDeleteStatuses.add(
+                                            ContactsUpdateStats.ERROR_CODE_APP_SEARCH_SYSTEM_ERROR
+                                                    + AppSearchResult.throwableToFailedResult(
+                                                                    throwable)
+                                                            .getResultCode());
+                                    if (shouldKeepUpdatingOnError) {
+                                        future.complete(null);
+                                    } else {
+                                        future.completeExceptionally(throwable);
+                                    }
+                                }
+                            });
+                    return future;
+                });
     }
 
     /**
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors. When enabled, the
-     *                                  returned future completes normally even when contacts could
-     *                                  not be retrieved.
+     *     should continue after encountering errors. When enabled, the returned future completes
+     *     normally even when contacts could not be retrieved.
      */
     @NonNull
     private CompletableFuture<AppSearchBatchResult> getContactsByIdAsync(
-            @NonNull GetByDocumentIdRequest request, boolean shouldKeepUpdatingOnError,
+            @NonNull GetByDocumentIdRequest request,
+            boolean shouldKeepUpdatingOnError,
             @NonNull ContactsUpdateStats updateStats) {
         Objects.requireNonNull(request);
-        return mAppSearchSessionFuture.thenCompose(appSearchSession -> {
-            CompletableFuture<AppSearchBatchResult> future = new CompletableFuture<>();
-            appSearchSession.getByDocumentId(request, mExecutor,
-                    new BatchResultCallback<>() {
-                        @Override
-                        public void onResult(AppSearchBatchResult<String, GenericDocument> result) {
-                            future.complete(result);
-                        }
+        return mAppSearchSessionFuture.thenCompose(
+                appSearchSession -> {
+                    CompletableFuture<AppSearchBatchResult> future = new CompletableFuture<>();
+                    appSearchSession.getByDocumentId(
+                            request,
+                            mExecutor,
+                            new BatchResultCallback<>() {
+                                @Override
+                                public void onResult(
+                                        AppSearchBatchResult<String, GenericDocument> result) {
+                                    future.complete(result);
+                                }
 
-                        @Override
-                        public void onSystemError(Throwable throwable) {
-                            Log.e(TAG, "Failed to get contacts", throwable);
-                            // Log a combined status code; ranges of the codes do not overlap
-                            // 10100 + 0-99
-                            updateStats.mUpdateStatuses.add(
-                                    ContactsUpdateStats.ERROR_CODE_APP_SEARCH_SYSTEM_ERROR
-                                            + AppSearchResult.throwableToFailedResult(
-                                            throwable).getResultCode());
-                            if (shouldKeepUpdatingOnError) {
-                                future.complete(new AppSearchBatchResult.Builder<>().build());
-                            } else {
-                                future.completeExceptionally(throwable);
-                            }
-                        }
-                    });
-            return future;
-        });
+                                @Override
+                                public void onSystemError(Throwable throwable) {
+                                    Log.e(TAG, "Failed to get contacts", throwable);
+                                    // Log a combined status code; ranges of the codes do not
+                                    // overlap
+                                    // 10100 + 0-99
+                                    updateStats.mUpdateStatuses.add(
+                                            ContactsUpdateStats.ERROR_CODE_APP_SEARCH_SYSTEM_ERROR
+                                                    + AppSearchResult.throwableToFailedResult(
+                                                                    throwable)
+                                                            .getResultCode());
+                                    if (shouldKeepUpdatingOnError) {
+                                        future.complete(
+                                                new AppSearchBatchResult.Builder<>().build());
+                                    } else {
+                                        future.completeExceptionally(throwable);
+                                    }
+                                }
+                            });
+                    return future;
+                });
     }
 
     /**
@@ -479,89 +552,101 @@
      */
     @NonNull
     public CompletableFuture<List<String>> getAllContactIdsAsync() {
-        return mAppSearchSessionFuture.thenCompose(appSearchSession -> {
-            SearchSpec allDocumentIdsSpec = new SearchSpec.Builder()
-                    .addFilterNamespaces(NAMESPACE_NAME)
-                    .addFilterSchemas(Person.SCHEMA_TYPE)
-                    .addProjection(Person.SCHEMA_TYPE, /*propertyPaths=*/ Collections.emptyList())
-                    .setResultCountPerPage(GET_CONTACT_IDS_PAGE_SIZE)
-                    .build();
-            SearchResults results =
-                    appSearchSession.search(/*queryExpression=*/ "", allDocumentIdsSpec);
-            List<String> allContactIds = new ArrayList<>();
-            return collectDocumentIdsFromAllPagesAsync(results, allContactIds)
-                    .thenCompose(unused -> {
-                        results.close();
-                        return CompletableFuture.supplyAsync(() -> allContactIds);
-                    });
-        });
+        return mAppSearchSessionFuture.thenCompose(
+                appSearchSession -> {
+                    SearchSpec allDocumentIdsSpec =
+                            new SearchSpec.Builder()
+                                    .addFilterNamespaces(NAMESPACE_NAME)
+                                    .addFilterSchemas(Person.SCHEMA_TYPE)
+                                    .addProjection(
+                                            Person.SCHEMA_TYPE,
+                                            /* propertyPaths= */ Collections.emptyList())
+                                    .setResultCountPerPage(GET_CONTACT_IDS_PAGE_SIZE)
+                                    .build();
+                    SearchResults results =
+                            appSearchSession.search(/* queryExpression= */ "", allDocumentIdsSpec);
+                    List<String> allContactIds = new ArrayList<>();
+                    return collectDocumentIdsFromAllPagesAsync(results, allContactIds)
+                            .thenCompose(
+                                    unused -> {
+                                        results.close();
+                                        return CompletableFuture.supplyAsync(() -> allContactIds);
+                                    });
+                });
     }
 
     /**
      * Gets {@link GenericDocument}s with only fingerprints projected for the requested contact ids.
      *
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors.
+     *     should continue after encountering errors.
      * @return A list containing the corresponding {@link GenericDocument} for the requested contact
-     *         ids in order. The entry is {@code null} if the requested contact id is not found in
-     *         AppSearch.
+     *     ids in order. The entry is {@code null} if the requested contact id is not found in
+     *     AppSearch.
      */
     @NonNull
     public CompletableFuture<List<GenericDocument>> getContactsWithFingerprintsAsync(
-            @NonNull List<String> ids, boolean shouldKeepUpdatingOnError,
+            @NonNull List<String> ids,
+            boolean shouldKeepUpdatingOnError,
             @NonNull ContactsUpdateStats updateStats) {
         Objects.requireNonNull(ids);
-        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder(
-                AppSearchHelper.NAMESPACE_NAME)
-                .addProjection(Person.SCHEMA_TYPE,
-                        Collections.singletonList(Person.PERSON_PROPERTY_FINGERPRINT))
-                .addIds(ids)
-                .build();
-        return getContactsByIdAsync(request, shouldKeepUpdatingOnError,
-                updateStats).thenCompose(appSearchBatchResult -> {
-            Map<String, GenericDocument> contactsExistInAppSearch =
-                    appSearchBatchResult.getSuccesses();
-            List<GenericDocument> docsWithFingerprints = new ArrayList<>(ids.size());
-            for (int i = 0; i < ids.size(); ++i) {
-                docsWithFingerprints.add(contactsExistInAppSearch.get(ids.get(i)));
-            }
-            return CompletableFuture.completedFuture(docsWithFingerprints);
-        });
+        GetByDocumentIdRequest request =
+                new GetByDocumentIdRequest.Builder(AppSearchHelper.NAMESPACE_NAME)
+                        .addProjection(
+                                Person.SCHEMA_TYPE,
+                                Collections.singletonList(Person.PERSON_PROPERTY_FINGERPRINT))
+                        .addIds(ids)
+                        .build();
+        return getContactsByIdAsync(request, shouldKeepUpdatingOnError, updateStats)
+                .thenCompose(
+                        appSearchBatchResult -> {
+                            Map<String, GenericDocument> contactsExistInAppSearch =
+                                    appSearchBatchResult.getSuccesses();
+                            List<GenericDocument> docsWithFingerprints =
+                                    new ArrayList<>(ids.size());
+                            for (int i = 0; i < ids.size(); ++i) {
+                                docsWithFingerprints.add(contactsExistInAppSearch.get(ids.get(i)));
+                            }
+                            return CompletableFuture.completedFuture(docsWithFingerprints);
+                        });
     }
 
     /**
      * Recursively pages through all search results and collects document IDs into given list.
      *
-     * @param results    Iterator for paging through the search results.
+     * @param results Iterator for paging through the search results.
      * @param contactIds List for collecting and returning document IDs.
      * @return A future indicating if more results might be available.
      */
     private CompletableFuture<Boolean> collectDocumentIdsFromAllPagesAsync(
-            @NonNull SearchResults results,
-            @NonNull List<String> contactIds) {
+            @NonNull SearchResults results, @NonNull List<String> contactIds) {
         Objects.requireNonNull(results);
         Objects.requireNonNull(contactIds);
 
         CompletableFuture<Boolean> future = new CompletableFuture<>();
-        results.getNextPage(mExecutor, callback -> {
-            if (!callback.isSuccess()) {
-                future.completeExceptionally(new AppSearchException(callback.getResultCode(),
-                        callback.getErrorMessage()));
-                return;
-            }
-            List<SearchResult> resultList = callback.getResultValue();
-            for (int i = 0; i < resultList.size(); i++) {
-                SearchResult result = resultList.get(i);
-                contactIds.add(result.getGenericDocument().getId());
-            }
-            future.complete(!resultList.isEmpty());
-        });
-        return future.thenCompose(moreResults -> {
-            // Recurse if there might be more results to page through.
-            if (moreResults) {
-                return collectDocumentIdsFromAllPagesAsync(results, contactIds);
-            }
-            return CompletableFuture.supplyAsync(() -> false);
-        });
+        results.getNextPage(
+                mExecutor,
+                callback -> {
+                    if (!callback.isSuccess()) {
+                        future.completeExceptionally(
+                                new AppSearchException(
+                                        callback.getResultCode(), callback.getErrorMessage()));
+                        return;
+                    }
+                    List<SearchResult> resultList = callback.getResultValue();
+                    for (int i = 0; i < resultList.size(); i++) {
+                        SearchResult result = resultList.get(i);
+                        contactIds.add(result.getGenericDocument().getId());
+                    }
+                    future.complete(!resultList.isEmpty());
+                });
+        return future.thenCompose(
+                moreResults -> {
+                    // Recurse if there might be more results to page through.
+                    if (moreResults) {
+                        return collectDocumentIdsFromAllPagesAsync(results, contactIds);
+                    }
+                    return CompletableFuture.supplyAsync(() -> false);
+                });
     }
 }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java b/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java
index 25e97b1..9b58b49 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactDataHandler.java
@@ -84,16 +84,16 @@
     }
 
     /**
-     * Adds the information of the current row from {@link ContactsContract.Data} table
-     * into the {@link PersonBuilderHelper}.
+     * Adds the information of the current row from {@link ContactsContract.Data} table into the
+     * {@link PersonBuilderHelper}.
      *
      * <p>By reading each row in the table, we will get the detailed information about a
      * Person(contact).
      *
      * @param builderHelper a helper to build the {@link Person}.
      */
-    public void convertCursorToPerson(@NonNull Cursor cursor,
-            @NonNull PersonBuilderHelper builderHelper) {
+    public void convertCursorToPerson(
+            @NonNull Cursor cursor, @NonNull PersonBuilderHelper builderHelper) {
         Objects.requireNonNull(cursor);
         Objects.requireNonNull(builderHelper);
 
@@ -155,8 +155,8 @@
 
         /** Adds the data into {@link PersonBuilderHelper}. */
         @Override
-        public final void addData(@NonNull PersonBuilderHelper builderHelper,
-                @NonNull Cursor cursor) {
+        public final void addData(
+                @NonNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor) {
             Objects.requireNonNull(builderHelper);
             Objects.requireNonNull(cursor);
 
@@ -166,8 +166,8 @@
             }
         }
 
-        protected abstract void addSingleColumnStringData(PersonBuilderHelper builderHelper,
-                String data);
+        protected abstract void addSingleColumnStringData(
+                PersonBuilderHelper builderHelper, String data);
     }
 
     private abstract static class ContactPointDataHandler extends DataHandler {
@@ -177,8 +177,10 @@
         private final String mLabelColumn;
 
         public ContactPointDataHandler(
-                @NonNull Resources resources, @NonNull String[] dataColumns,
-                @NonNull String typeColumn, @NonNull String labelColumn) {
+                @NonNull Resources resources,
+                @NonNull String[] dataColumns,
+                @NonNull String typeColumn,
+                @NonNull String labelColumn) {
             mResources = Objects.requireNonNull(resources);
             mDataColumns = Objects.requireNonNull(dataColumns);
             mTypeColumn = Objects.requireNonNull(typeColumn);
@@ -200,12 +202,12 @@
         }
 
         /**
-         * Adds the data for ContactsPoint(email, telephone, postal addresses) into
-         * {@link Person.Builder}.
+         * Adds the data for ContactsPoint(email, telephone, postal addresses) into {@link
+         * Person.Builder}.
          */
         @Override
-        public final void addData(@NonNull PersonBuilderHelper builderHelper,
-                @NonNull Cursor cursor) {
+        public final void addData(
+                @NonNull PersonBuilderHelper builderHelper, @NonNull Cursor cursor) {
             Objects.requireNonNull(builderHelper);
             Objects.requireNonNull(cursor);
 
@@ -220,8 +222,8 @@
             if (!data.isEmpty()) {
                 // get the corresponding label to the type.
                 int type = getColumnInt(cursor, mTypeColumn);
-                String label = getTypeLabel(mResources, type,
-                        getColumnString(cursor, mLabelColumn));
+                String label =
+                        getTypeLabel(mResources, type, getColumnString(cursor, mLabelColumn));
                 addContactPointData(builderHelper, label, data);
             }
         }
@@ -233,8 +235,8 @@
          * Adds the information in the {@link Person.Builder}.
          *
          * @param builderHelper a helper to build the {@link Person}.
-         * @param label         the corresponding label to the {@code type} for the data.
-         * @param data          data read from the designed columns in the row.
+         * @param label the corresponding label to the {@code type} for the data.
+         * @param data data read from the designed columns in the row.
          */
         protected abstract void addContactPointData(
                 PersonBuilderHelper builderHelper, String label, Map<String, String> data);
@@ -242,7 +244,7 @@
 
     private static final class EmailDataHandler extends ContactPointDataHandler {
         private static final String[] COLUMNS = {
-                Email.ADDRESS,
+            Email.ADDRESS,
         };
 
         public EmailDataHandler(@NonNull Resources resources) {
@@ -253,16 +255,15 @@
          * Adds the Email information in the {@link Person.Builder}.
          *
          * @param builderHelper a builder to build the {@link Person}.
-         * @param label         The corresponding label to the {@code type}. E.g. {@link
-         *                      com.android.internal.R.string#emailTypeHome} to {@link
-         *                      Email#TYPE_HOME} or custom label for the data if {@code type} is
-         *                      {@link
-         *                      Email#TYPE_CUSTOM}.
-         * @param data          data read from the designed column {@code Email.ADDRESS} in the row.
+         * @param label The corresponding label to the {@code type}. E.g. {@link
+         *     com.android.internal.R.string#emailTypeHome} to {@link Email#TYPE_HOME} or custom
+         *     label for the data if {@code type} is {@link Email#TYPE_CUSTOM}.
+         * @param data data read from the designed column {@code Email.ADDRESS} in the row.
          */
         @Override
         protected void addContactPointData(
-                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
+                @NonNull PersonBuilderHelper builderHelper,
+                @NonNull String label,
                 @NonNull Map<String, String> data) {
             Objects.requireNonNull(builderHelper);
             Objects.requireNonNull(data);
@@ -275,8 +276,8 @@
 
         @NonNull
         @Override
-        protected String getTypeLabel(@NonNull Resources resources, int type,
-                @Nullable String label) {
+        protected String getTypeLabel(
+                @NonNull Resources resources, int type, @Nullable String label) {
             Objects.requireNonNull(resources);
             return Email.getTypeLabel(resources, type, label).toString();
         }
@@ -284,8 +285,7 @@
 
     private static final class PhoneHandler extends ContactPointDataHandler {
         private static final String[] COLUMNS = {
-                Phone.NUMBER,
-                Phone.NORMALIZED_NUMBER,
+            Phone.NUMBER, Phone.NORMALIZED_NUMBER,
         };
 
         private final Resources mResources;
@@ -299,15 +299,15 @@
          * Adds the phone number information in the {@link Person.Builder}.
          *
          * @param builderHelper helper to build the {@link Person}.
-         * @param label         corresponding label to {@code type}. E.g. {@link
-         *                      com.android.internal.R.string#phoneTypeHome} to {@link
-         *                      Phone#TYPE_HOME}, or custom label for the data if {@code type} is
-         *                      {@link Phone#TYPE_CUSTOM}.
-         * @param data          data read from the designed columns {@link Phone#NUMBER} in the row.
+         * @param label corresponding label to {@code type}. E.g. {@link
+         *     com.android.internal.R.string#phoneTypeHome} to {@link Phone#TYPE_HOME}, or custom
+         *     label for the data if {@code type} is {@link Phone#TYPE_CUSTOM}.
+         * @param data data read from the designed columns {@link Phone#NUMBER} in the row.
          */
         @Override
         protected void addContactPointData(
-                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
+                @NonNull PersonBuilderHelper builderHelper,
+                @NonNull String label,
                 @NonNull Map<String, String> data) {
             Objects.requireNonNull(builderHelper);
             Objects.requireNonNull(data);
@@ -329,8 +329,8 @@
             // efforts, depending on the locales available in the current configuration on the
             // system.
             Set<String> phoneNumberVariants =
-                    ContactsIndexerPhoneNumberUtils.createPhoneNumberVariants(mResources,
-                            phoneNumberOriginal, phoneNumberE164FromCP2);
+                    ContactsIndexerPhoneNumberUtils.createPhoneNumberVariants(
+                            mResources, phoneNumberOriginal, phoneNumberE164FromCP2);
 
             phoneNumberVariants.remove(phoneNumberOriginal);
             for (String variant : phoneNumberVariants) {
@@ -342,8 +342,8 @@
 
         @NonNull
         @Override
-        protected String getTypeLabel(@NonNull Resources resources, int type,
-                @Nullable String label) {
+        protected String getTypeLabel(
+                @NonNull Resources resources, int type, @Nullable String label) {
             Objects.requireNonNull(resources);
             return Phone.getTypeLabel(resources, type, label).toString();
         }
@@ -351,31 +351,27 @@
 
     private static final class StructuredPostalHandler extends ContactPointDataHandler {
         private static final String[] COLUMNS = {
-                StructuredPostal.FORMATTED_ADDRESS,
+            StructuredPostal.FORMATTED_ADDRESS,
         };
 
         public StructuredPostalHandler(@NonNull Resources resources) {
-            super(
-                    resources,
-                    COLUMNS,
-                    StructuredPostal.TYPE,
-                    StructuredPostal.LABEL);
+            super(resources, COLUMNS, StructuredPostal.TYPE, StructuredPostal.LABEL);
         }
 
         /**
          * Adds the postal address information in the {@link Person.Builder}.
          *
          * @param builderHelper helper to build the {@link Person}.
-         * @param label         corresponding label to {@code type}. E.g. {@link
-         *                      com.android.internal.R.string#postalTypeHome} to {@link
-         *                      StructuredPostal#TYPE_HOME}, or custom label for the data if {@code
-         *                      type} is {@link StructuredPostal#TYPE_CUSTOM}.
-         * @param data          data read from the designed column
-         *                      {@link StructuredPostal#FORMATTED_ADDRESS} in the row.
+         * @param label corresponding label to {@code type}. E.g. {@link
+         *     com.android.internal.R.string#postalTypeHome} to {@link StructuredPostal#TYPE_HOME},
+         *     or custom label for the data if {@code type} is {@link StructuredPostal#TYPE_CUSTOM}.
+         * @param data data read from the designed column {@link StructuredPostal#FORMATTED_ADDRESS}
+         *     in the row.
          */
         @Override
         protected void addContactPointData(
-                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
+                @NonNull PersonBuilderHelper builderHelper,
+                @NonNull String label,
                 @NonNull Map<String, String> data) {
             Objects.requireNonNull(builderHelper);
             Objects.requireNonNull(data);
@@ -388,8 +384,8 @@
 
         @NonNull
         @Override
-        protected String getTypeLabel(@NonNull Resources resources, int type,
-                @Nullable String label) {
+        protected String getTypeLabel(
+                @NonNull Resources resources, int type, @Nullable String label) {
             Objects.requireNonNull(resources);
             return StructuredPostal.getTypeLabel(resources, type, label).toString();
         }
@@ -401,8 +397,8 @@
         }
 
         @Override
-        protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
-                @NonNull String data) {
+        protected void addSingleColumnStringData(
+                @NonNull PersonBuilderHelper builder, @NonNull String data) {
             Objects.requireNonNull(builder);
             Objects.requireNonNull(data);
             builder.getPersonBuilder().addAdditionalName(Person.TYPE_NICKNAME, data);
@@ -411,12 +407,12 @@
 
     private static final class StructuredNameHandler extends DataHandler {
         private static final String[] COLUMNS = {
-                Data.RAW_CONTACT_ID,
-                Data.NAME_RAW_CONTACT_ID,
-                // Only those three fields we need to set in the builder.
-                StructuredName.GIVEN_NAME,
-                StructuredName.MIDDLE_NAME,
-                StructuredName.FAMILY_NAME,
+            Data.RAW_CONTACT_ID,
+            Data.NAME_RAW_CONTACT_ID,
+            // Only those three fields we need to set in the builder.
+            StructuredName.GIVEN_NAME,
+            StructuredName.MIDDLE_NAME,
+            StructuredName.FAMILY_NAME,
         };
 
         /** Adds the columns needed for the {@code DataHandler}. */
@@ -456,9 +452,7 @@
 
     private static final class OrganizationDataHandler extends DataHandler {
         private static final String[] COLUMNS = {
-                Organization.TITLE,
-                Organization.DEPARTMENT,
-                Organization.COMPANY,
+            Organization.TITLE, Organization.DEPARTMENT, Organization.COMPANY,
         };
 
         private final StringBuilder mStringBuilder = new StringBuilder();
@@ -490,9 +484,7 @@
 
     private static final class RelationDataHandler extends DataHandler {
         private static final String[] COLUMNS = {
-                Relation.NAME,
-                Relation.TYPE,
-                Relation.LABEL,
+            Relation.NAME, Relation.TYPE, Relation.LABEL,
         };
 
         private final Resources mResources;
@@ -531,11 +523,11 @@
         }
 
         @Override
-        protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
-                @NonNull String data) {
+        protected void addSingleColumnStringData(
+                @NonNull PersonBuilderHelper builder, @NonNull String data) {
             Objects.requireNonNull(builder);
             Objects.requireNonNull(data);
             builder.getPersonBuilder().addNote(data);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java
index 8737cca..11751aa 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerConfig.java
@@ -71,8 +71,6 @@
      */
     boolean shouldIndexFirstMiddleAndLastNames();
 
-    /**
-     * Returns whether full and delta updates should continue on error.
-     */
+    /** Returns whether full and delta updates should continue on error. */
     boolean shouldKeepUpdatingOnError();
 }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java
index 9739c2b..bc675e1 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerImpl.java
@@ -54,31 +54,32 @@
     static final int NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH = 500;
     // Common columns needed for all kinds of mime types
     static final String[] COMMON_NEEDED_COLUMNS = {
-            ContactsContract.Data.CONTACT_ID,
-            ContactsContract.Data.LOOKUP_KEY,
-            ContactsContract.Data.PHOTO_THUMBNAIL_URI,
-            ContactsContract.Data.DISPLAY_NAME_PRIMARY,
-            ContactsContract.Data.PHONETIC_NAME,
-            ContactsContract.Data.RAW_CONTACT_ID,
-            ContactsContract.Data.STARRED,
-            ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP
+        ContactsContract.Data.CONTACT_ID,
+        ContactsContract.Data.LOOKUP_KEY,
+        ContactsContract.Data.PHOTO_THUMBNAIL_URI,
+        ContactsContract.Data.DISPLAY_NAME_PRIMARY,
+        ContactsContract.Data.PHONETIC_NAME,
+        ContactsContract.Data.RAW_CONTACT_ID,
+        ContactsContract.Data.STARRED,
+        ContactsContract.Data.CONTACT_LAST_UPDATED_TIMESTAMP
     };
     // The order for the results returned from CP2.
-    static final String ORDER_BY = ContactsContract.Data.CONTACT_ID
-            // MUST sort by CONTACT_ID first for our iteration to work
-            + ","
-            // Whether this is the primary entry of its kind for the aggregate
-            // contact it belongs to.
-            + ContactsContract.Data.IS_SUPER_PRIMARY
-            + " DESC"
-            // Then rank by importance.
-            + ","
-            // Whether this is the primary entry of its kind for the raw contact it
-            // belongs to.
-            + ContactsContract.Data.IS_PRIMARY
-            + " DESC"
-            + ","
-            + ContactsContract.Data.RAW_CONTACT_ID;
+    static final String ORDER_BY =
+            ContactsContract.Data.CONTACT_ID
+                    // MUST sort by CONTACT_ID first for our iteration to work
+                    + ","
+                    // Whether this is the primary entry of its kind for the aggregate
+                    // contact it belongs to.
+                    + ContactsContract.Data.IS_SUPER_PRIMARY
+                    + " DESC"
+                    // Then rank by importance.
+                    + ","
+                    // Whether this is the primary entry of its kind for the raw contact it
+                    // belongs to.
+                    + ContactsContract.Data.IS_PRIMARY
+                    + " DESC"
+                    + ","
+                    + ContactsContract.Data.RAW_CONTACT_ID;
 
     private final Context mContext;
     private final ContactDataHandler mContactDataHandler;
@@ -101,11 +102,11 @@
      * <p>It deletes removed contacts, inserts newly-added ones, and updates existing ones in the
      * Person corpus in AppSearch.
      *
-     * @param wantedContactIds      ids for contacts to be updated.
-     * @param unWantedIds           ids for contacts to be deleted.
-     * @param updateStats           to hold the counters for the update.
+     * @param wantedContactIds ids for contacts to be updated.
+     * @param unWantedIds ids for contacts to be deleted.
+     * @param updateStats to hold the counters for the update.
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors.
+     *     should continue after encountering errors.
      */
     public CompletableFuture<Void> updatePersonCorpusAsync(
             @NonNull List<String> wantedContactIds,
@@ -116,16 +117,23 @@
         Objects.requireNonNull(unWantedIds);
         Objects.requireNonNull(updateStats);
 
-        return batchRemoveContactsAsync(unWantedIds, updateStats,
-                shouldKeepUpdatingOnError).exceptionally(t -> {
-            // Since we update the timestamps no matter the update succeeds or fails, we can
-            // always try to do the indexing. Updating lastDeltaUpdateTimestamps without doing
-            // indexing seems odd.
-            // So catch the exception here for deletion, and we can keep doing the indexing.
-            Log.w(TAG, "Error occurred during batch delete", t);
-            return null;
-        }).thenCompose(x -> batchUpdateContactsAsync(wantedContactIds, updateStats,
-                shouldKeepUpdatingOnError));
+        return batchRemoveContactsAsync(unWantedIds, updateStats, shouldKeepUpdatingOnError)
+                .exceptionally(
+                        t -> {
+                            // Since we update the timestamps no matter the update succeeds or
+                            // fails, we can
+                            // always try to do the indexing. Updating lastDeltaUpdateTimestamps
+                            // without doing
+                            // indexing seems odd.
+                            // So catch the exception here for deletion, and we can keep doing the
+                            // indexing.
+                            Log.w(TAG, "Error occurred during batch delete", t);
+                            return null;
+                        })
+                .thenCompose(
+                        x ->
+                                batchUpdateContactsAsync(
+                                        wantedContactIds, updateStats, shouldKeepUpdatingOnError));
     }
 
     /**
@@ -133,7 +141,7 @@
      *
      * @param updateStats to hold the counters for the remove.
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors.
+     *     should continue after encountering errors.
      */
     @VisibleForTesting
     CompletableFuture<Void> batchRemoveContactsAsync(
@@ -145,16 +153,22 @@
         int unWantedSize = unWantedIds.size();
         updateStats.mTotalContactsToBeDeleted += unWantedSize;
         while (startIndex < unWantedSize) {
-            int endIndex = Math.min(startIndex + NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH,
-                    unWantedSize);
+            int endIndex =
+                    Math.min(
+                            startIndex + NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH,
+                            unWantedSize);
             Collection<String> currentContactIds = unWantedIds.subList(startIndex, endIndex);
             // If any removeContactsByIdAsync in the future-chain completes exceptionally, all
             // futures following it will not run and will instead complete exceptionally. However,
             // when shouldKeepUpdatingOnError is true, removeContactsByIdAsync avoids completing
             // exceptionally.
-            batchRemoveFuture = batchRemoveFuture.thenCompose(
-                    x -> mAppSearchHelper.removeContactsByIdAsync(currentContactIds, updateStats,
-                            shouldKeepUpdatingOnError));
+            batchRemoveFuture =
+                    batchRemoveFuture.thenCompose(
+                            x ->
+                                    mAppSearchHelper.removeContactsByIdAsync(
+                                            currentContactIds,
+                                            updateStats,
+                                            shouldKeepUpdatingOnError));
             startIndex = endIndex;
         }
         return batchRemoveFuture;
@@ -165,9 +179,8 @@
      *
      * @param updateStats to hold the counters for the update.
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors. When enabled and
-     *                                  we fail to query CP@ for a batch of contacts, we continue
-     *                                  onto the next batch instead of stopping.
+     *     should continue after encountering errors. When enabled and we fail to query CP@ for a
+     *     batch of contacts, we continue onto the next batch instead of stopping.
      */
     CompletableFuture<Void> batchUpdateContactsAsync(
             @NonNull final List<String> wantedContactIds,
@@ -185,30 +198,41 @@
         // simultaneous updates would use the same ContactsBatcher, leading to updates sometimes
         // indexing each other's contacts and messing up the metrics/counts for the number of
         // succeeded/skipped contacts.
-        ContactsBatcher contactsBatcher = new ContactsBatcher(mAppSearchHelper,
-                NUM_UPDATED_CONTACTS_PER_BATCH_FOR_APPSEARCH, shouldKeepUpdatingOnError);
+        ContactsBatcher contactsBatcher =
+                new ContactsBatcher(
+                        mAppSearchHelper,
+                        NUM_UPDATED_CONTACTS_PER_BATCH_FOR_APPSEARCH,
+                        shouldKeepUpdatingOnError);
         while (startIndex < wantedIdListSize) {
-            int endIndex = Math.min(startIndex + NUM_CONTACTS_PER_BATCH_FOR_CP2,
-                    wantedIdListSize);
+            int endIndex = Math.min(startIndex + NUM_CONTACTS_PER_BATCH_FOR_CP2, wantedIdListSize);
             Collection<String> currentContactIds = wantedContactIds.subList(startIndex, endIndex);
             // Read NUM_CONTACTS_PER_BATCH contacts every time from CP2.
-            String selection = ContactsContract.Data.CONTACT_ID + " IN (" + TextUtils.join(
-                    /*delimiter=*/ ",", currentContactIds) + ")";
+            String selection =
+                    ContactsContract.Data.CONTACT_ID
+                            + " IN ("
+                            + TextUtils.join(/* delimiter= */ ",", currentContactIds)
+                            + ")";
             startIndex = endIndex;
             try {
                 // For our iteration work, we must sort the result by contact_id first.
-                Cursor cursor = mContext.getContentResolver().query(
-                        ContactsContract.Data.CONTENT_URI,
-                        mProjection,
-                        selection, /*selectionArgs=*/null,
-                        ORDER_BY);
+                Cursor cursor =
+                        mContext.getContentResolver()
+                                .query(
+                                        ContactsContract.Data.CONTENT_URI,
+                                        mProjection,
+                                        selection,
+                                        /* selectionArgs= */ null,
+                                        ORDER_BY);
                 if (cursor == null) {
                     updateStats.mUpdateStatuses.add(ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
                     Log.w(TAG, "Cursor was returned as null while querying CP2.");
                     if (!shouldKeepUpdatingOnError) {
-                        return future.thenCompose(x -> CompletableFuture.failedFuture(
-                                new IllegalStateException(
-                                        "Cursor was returned as null while querying CP2.")));
+                        return future.thenCompose(
+                                x ->
+                                        CompletableFuture.failedFuture(
+                                                new IllegalStateException(
+                                                        "Cursor was returned as null while querying"
+                                                                + " CP2.")));
                     }
                 } else {
                     // If any indexContactsFromCursorAsync in the future-chain completes
@@ -216,13 +240,20 @@
                     // complete exceptionally. However, when shouldKeepUpdatingOnError is true,
                     // indexContactsFromCursorAsync avoids completing exceptionally except for
                     // AppSearchResult#RESULT_OUT_OF_SPACE.
-                    future = future.thenCompose(
-                            x -> indexContactsFromCursorAsync(cursor, updateStats, contactsBatcher,
-                                    shouldKeepUpdatingOnError)
-                    ).whenComplete((x, t) -> {
-                        // ensure the cursor is closed even when the future-chain fails
-                        cursor.close();
-                    });
+                    future =
+                            future.thenCompose(
+                                            x ->
+                                                    indexContactsFromCursorAsync(
+                                                            cursor,
+                                                            updateStats,
+                                                            contactsBatcher,
+                                                            shouldKeepUpdatingOnError))
+                                    .whenComplete(
+                                            (x, t) -> {
+                                                // ensure the cursor is closed even when the
+                                                // future-chain fails
+                                                cursor.close();
+                                            });
                 }
             } catch (RuntimeException e) {
                 // The ContactsProvider sometimes propagates RuntimeExceptions to us
@@ -241,29 +272,30 @@
     }
 
     /**
-     * Reads through cursor, converts the contacts to AppSearch documents, and indexes the
-     * documents into AppSearch.
+     * Reads through cursor, converts the contacts to AppSearch documents, and indexes the documents
+     * into AppSearch.
      *
-     * @param cursor      pointing to the contacts read from CP2.
+     * @param cursor pointing to the contacts read from CP2.
      * @param updateStats to hold the counters for the update.
      * @param contactsBatcher the batcher that indexes the contacts for this update.
      * @param shouldKeepUpdatingOnError ContactsIndexer flag controlling whether or not updates
-     *                                  should continue after encountering errors. When enabled and
-     *                                  an exception is thrown, stops further indexing and flushes
-     *                                  the current batch of contacts but does not return a failed
-     *                                  future.
+     *     should continue after encountering errors. When enabled and an exception is thrown, stops
+     *     further indexing and flushes the current batch of contacts but does not return a failed
+     *     future.
      */
-    private CompletableFuture<Void> indexContactsFromCursorAsync(@NonNull Cursor cursor,
-            @NonNull ContactsUpdateStats updateStats, @NonNull ContactsBatcher contactsBatcher,
+    private CompletableFuture<Void> indexContactsFromCursorAsync(
+            @NonNull Cursor cursor,
+            @NonNull ContactsUpdateStats updateStats,
+            @NonNull ContactsBatcher contactsBatcher,
             boolean shouldKeepUpdatingOnError) {
         Objects.requireNonNull(cursor);
         try {
             int contactIdIndex = cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID);
             int lookupKeyIndex = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
-            int thumbnailUriIndex = cursor.getColumnIndex(
-                    ContactsContract.Data.PHOTO_THUMBNAIL_URI);
-            int displayNameIndex = cursor.getColumnIndex(
-                    ContactsContract.Data.DISPLAY_NAME_PRIMARY);
+            int thumbnailUriIndex =
+                    cursor.getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI);
+            int displayNameIndex =
+                    cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME_PRIMARY);
             int starredIndex = cursor.getColumnIndex(ContactsContract.Data.STARRED);
             int phoneticNameIndex = cursor.getColumnIndex(ContactsContract.Data.PHONETIC_NAME);
             long currentContactId = -1;
@@ -289,14 +321,19 @@
                         // name is missing.
                         displayName = "";
                     }
-                    personBuilder = new Person.Builder(AppSearchHelper.NAMESPACE_NAME,
-                            String.valueOf(contactId), displayName);
+                    personBuilder =
+                            new Person.Builder(
+                                    AppSearchHelper.NAMESPACE_NAME,
+                                    String.valueOf(contactId),
+                                    displayName);
                     String imageUri = getStringFromCursor(cursor, thumbnailUriIndex);
                     String lookupKey = getStringFromCursor(cursor, lookupKeyIndex);
                     boolean starred = starredIndex != -1 && cursor.getInt(starredIndex) != 0;
-                    Uri lookupUri = lookupKey != null ?
-                            ContactsContract.Contacts.getLookupUri(currentContactId, lookupKey)
-                            : null;
+                    Uri lookupUri =
+                            lookupKey != null
+                                    ? ContactsContract.Contacts.getLookupUri(
+                                            currentContactId, lookupKey)
+                                    : null;
                     personBuilder.setIsImportant(starred);
                     if (lookupUri != null) {
                         personBuilder.setExternalUri(lookupUri);
@@ -311,9 +348,9 @@
                     // Always use current system timestamp first. If that contact already exists
                     // in AppSearch, the creationTimestamp for this doc will be reset with the
                     // original value stored in AppSearch during performDiffAsync.
-                    personBuilderHelper = new PersonBuilderHelper(String.valueOf(contactId),
-                            personBuilder)
-                            .setCreationTimestampMillis(System.currentTimeMillis());
+                    personBuilderHelper =
+                            new PersonBuilderHelper(String.valueOf(contactId), personBuilder)
+                                    .setCreationTimestampMillis(System.currentTimeMillis());
                 }
                 if (personBuilderHelper != null) {
                     mContactDataHandler.convertCursorToPerson(cursor, personBuilderHelper);
@@ -332,8 +369,9 @@
             // TODO(b/203605504) see if we could catch more specific exceptions/errors.
             Log.e(TAG, "Error while indexing documents from the cursor", e);
             if (!shouldKeepUpdatingOnError) {
-                return contactsBatcher.flushAsync(updateStats).thenCompose(
-                        x -> CompletableFuture.failedFuture(e));
+                return contactsBatcher
+                        .flushAsync(updateStats)
+                        .thenCompose(x -> CompletableFuture.failedFuture(e));
             }
         }
 
@@ -370,20 +408,22 @@
 
         /**
          * Batch size for both {@link #mPendingDiffContactBuilders} and {@link
-         * #mPendingIndexContacts}. It
-         * is strictly followed by {@link #mPendingDiffContactBuilders}. But for {@link
-         * #mPendingIndexContacts}, when we merge the former set into {@link
-         * #mPendingIndexContacts}, it could exceed this limit. At maximum it could hold 2 *
-         * {@link #mBatchSize} contacts before cleared.
+         * #mPendingIndexContacts}. It is strictly followed by {@link #mPendingDiffContactBuilders}.
+         * But for {@link #mPendingIndexContacts}, when we merge the former set into {@link
+         * #mPendingIndexContacts}, it could exceed this limit. At maximum it could hold 2 * {@link
+         * #mBatchSize} contacts before cleared.
          */
         private final int mBatchSize;
+
         private final AppSearchHelper mAppSearchHelper;
         private final boolean mShouldKeepUpdatingOnError;
 
         private CompletableFuture<Void> mIndexContactsCompositeFuture =
                 CompletableFuture.completedFuture(null);
 
-        ContactsBatcher(@NonNull AppSearchHelper appSearchHelper, int batchSize,
+        ContactsBatcher(
+                @NonNull AppSearchHelper appSearchHelper,
+                int batchSize,
                 boolean shouldKeepUpdatingOnError) {
             mAppSearchHelper = Objects.requireNonNull(appSearchHelper);
             mBatchSize = batchSize;
@@ -406,7 +446,8 @@
             return mPendingIndexContacts.size();
         }
 
-        public void add(@NonNull PersonBuilderHelper builderHelper,
+        public void add(
+                @NonNull PersonBuilderHelper builderHelper,
                 @NonNull ContactsUpdateStats updateStats) {
             Objects.requireNonNull(builderHelper);
             mPendingDiffContactBuilders.add(builderHelper);
@@ -415,14 +456,19 @@
                 // list for batching
                 List<PersonBuilderHelper> pendingDiffContactBuilders = mPendingDiffContactBuilders;
                 mPendingDiffContactBuilders = new ArrayList<>(mBatchSize);
-                mIndexContactsCompositeFuture = mIndexContactsCompositeFuture
-                        .thenCompose(x -> performDiffAsync(pendingDiffContactBuilders, updateStats))
-                        .thenCompose(y -> {
-                            if (mPendingIndexContacts.size() >= mBatchSize) {
-                                return flushPendingIndexAsync(updateStats);
-                            }
-                            return CompletableFuture.completedFuture(null);
-                        });
+                mIndexContactsCompositeFuture =
+                        mIndexContactsCompositeFuture
+                                .thenCompose(
+                                        x ->
+                                                performDiffAsync(
+                                                        pendingDiffContactBuilders, updateStats))
+                                .thenCompose(
+                                        y -> {
+                                            if (mPendingIndexContacts.size() >= mBatchSize) {
+                                                return flushPendingIndexAsync(updateStats);
+                                            }
+                                            return CompletableFuture.completedFuture(null);
+                                        });
             }
         }
 
@@ -432,9 +478,13 @@
                 // list for batching
                 List<PersonBuilderHelper> pendingDiffContactBuilders = mPendingDiffContactBuilders;
                 mPendingDiffContactBuilders = new ArrayList<>(mBatchSize);
-                mIndexContactsCompositeFuture = mIndexContactsCompositeFuture
-                        .thenCompose(x -> performDiffAsync(pendingDiffContactBuilders, updateStats))
-                        .thenCompose(y -> flushPendingIndexAsync(updateStats));
+                mIndexContactsCompositeFuture =
+                        mIndexContactsCompositeFuture
+                                .thenCompose(
+                                        x ->
+                                                performDiffAsync(
+                                                        pendingDiffContactBuilders, updateStats))
+                                .thenCompose(y -> flushPendingIndexAsync(updateStats));
             }
 
             CompletableFuture<Void> flushFuture = mIndexContactsCompositeFuture;
@@ -459,55 +509,58 @@
             // In this case, we may unnecessarily update some contacts in the following step, but
             // some unnecessary updates is better than no updates and should not cause a significant
             // impact on performance.
-            return mAppSearchHelper.getContactsWithFingerprintsAsync(ids,
-                            mShouldKeepUpdatingOnError, updateStats)
-                    .thenCompose(contactsWithFingerprints -> {
-                        List<Person> contactsToBeIndexed = new ArrayList<>(
-                                pendingDiffContactBuilders.size());
-                        // Before indexing a contact into AppSearch, we will check if the
-                        // contact with same id exists, and whether the fingerprint has
-                        // changed. If fingerprint has not been changed for the same
-                        // contact, we won't index it.
-                        for (int i = 0; i < pendingDiffContactBuilders.size(); ++i) {
-                            PersonBuilderHelper builderHelper =
-                                    pendingDiffContactBuilders.get(i);
-                            GenericDocument doc = contactsWithFingerprints.get(i);
-                            byte[] oldFingerprint =
-                                    doc != null ? doc.getPropertyBytes(
-                                            Person.PERSON_PROPERTY_FINGERPRINT) : null;
-                            long docCreationTimestampMillis =
-                                    doc != null ? doc.getCreationTimestampMillis()
-                                            : -1;
-                            if (oldFingerprint != null) {
-                                // We already have this contact in AppSearch. Reset the
-                                // creationTimestamp here with the original one.
-                                builderHelper.setCreationTimestampMillis(
-                                        docCreationTimestampMillis);
-                                Person person = builderHelper.buildPerson();
-                                if (!Arrays.equals(person.getFingerprint(),
-                                        oldFingerprint)) {
-                                    contactsToBeIndexed.add(person);
-                                } else {
-                                    // Fingerprint is same. So this update is skipped.
-                                    ++updateStats.mContactsUpdateSkippedCount;
+            return mAppSearchHelper
+                    .getContactsWithFingerprintsAsync(ids, mShouldKeepUpdatingOnError, updateStats)
+                    .thenCompose(
+                            contactsWithFingerprints -> {
+                                List<Person> contactsToBeIndexed =
+                                        new ArrayList<>(pendingDiffContactBuilders.size());
+                                // Before indexing a contact into AppSearch, we will check if the
+                                // contact with same id exists, and whether the fingerprint has
+                                // changed. If fingerprint has not been changed for the same
+                                // contact, we won't index it.
+                                for (int i = 0; i < pendingDiffContactBuilders.size(); ++i) {
+                                    PersonBuilderHelper builderHelper =
+                                            pendingDiffContactBuilders.get(i);
+                                    GenericDocument doc = contactsWithFingerprints.get(i);
+                                    byte[] oldFingerprint =
+                                            doc != null
+                                                    ? doc.getPropertyBytes(
+                                                            Person.PERSON_PROPERTY_FINGERPRINT)
+                                                    : null;
+                                    long docCreationTimestampMillis =
+                                            doc != null ? doc.getCreationTimestampMillis() : -1;
+                                    if (oldFingerprint != null) {
+                                        // We already have this contact in AppSearch. Reset the
+                                        // creationTimestamp here with the original one.
+                                        builderHelper.setCreationTimestampMillis(
+                                                docCreationTimestampMillis);
+                                        Person person = builderHelper.buildPerson();
+                                        if (!Arrays.equals(
+                                                person.getFingerprint(), oldFingerprint)) {
+                                            contactsToBeIndexed.add(person);
+                                        } else {
+                                            // Fingerprint is same. So this update is skipped.
+                                            ++updateStats.mContactsUpdateSkippedCount;
+                                        }
+                                    } else {
+                                        // New contact.
+                                        ++updateStats.mNewContactsToBeUpdated;
+                                        contactsToBeIndexed.add(builderHelper.buildPerson());
+                                    }
                                 }
-                            } else {
-                                // New contact.
-                                ++updateStats.mNewContactsToBeUpdated;
-                                contactsToBeIndexed.add(builderHelper.buildPerson());
-                            }
-                        }
-                        mPendingIndexContacts.addAll(contactsToBeIndexed);
-                        return CompletableFuture.completedFuture(null);
-                    });
+                                mPendingIndexContacts.addAll(contactsToBeIndexed);
+                                return CompletableFuture.completedFuture(null);
+                            });
         }
 
         /** Flushes the contacts batched in {@link #mPendingIndexContacts} to AppSearch. */
         private CompletableFuture<Void> flushPendingIndexAsync(
                 @NonNull ContactsUpdateStats updateStats) {
             if (mPendingIndexContacts.size() > 0) {
-                CompletableFuture<Void> future = mAppSearchHelper.indexContactsAsync(
-                        mPendingIndexContacts, updateStats, mShouldKeepUpdatingOnError);
+                CompletableFuture<Void> future =
+                        mAppSearchHelper.indexContactsAsync(
+                                mPendingIndexContacts, updateStats, mShouldKeepUpdatingOnError);
                 mPendingIndexContacts.clear();
                 return future;
             }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceConfig.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceConfig.java
new file mode 100644
index 0000000..8484ae9
--- /dev/null
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceConfig.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.contactsindexer;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.indexer.IndexerLocalService;
+import com.android.server.appsearch.indexer.IndexerMaintenanceConfig;
+
+/** Singleton class containing configuration for the contacts indexer maintenance task. */
+public class ContactsIndexerMaintenanceConfig implements IndexerMaintenanceConfig {
+    @VisibleForTesting
+    static final int MIN_CONTACTS_INDEXER_JOB_ID = 16942831; // corresponds to ag/16942831
+
+    public static final IndexerMaintenanceConfig INSTANCE = new ContactsIndexerMaintenanceConfig();
+
+    /** Enforces singleton class pattern. */
+    private ContactsIndexerMaintenanceConfig() {}
+
+    @NonNull
+    @Override
+    public Class<? extends IndexerLocalService> getLocalService() {
+        return ContactsIndexerManagerService.LocalService.class;
+    }
+
+    @Override
+    public int getMinJobId() {
+        return MIN_CONTACTS_INDEXER_JOB_ID;
+    }
+}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceService.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceService.java
index bd07114..ebf8402 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceService.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceService.java
@@ -16,257 +16,13 @@
 
 package com.android.server.appsearch.contactsindexer;
 
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.app.appsearch.annotation.CanIgnoreReturnValue;
-import android.app.appsearch.util.LogUtil;
-import android.app.job.JobInfo;
-import android.app.job.JobParameters;
-import android.app.job.JobScheduler;
-import android.app.job.JobService;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.CancellationSignal;
-import android.os.PersistableBundle;
-import android.os.UserHandle;
-import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
 
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.LocalManagerRegistry;
-import com.android.server.SystemService;
-
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-public class ContactsIndexerMaintenanceService extends JobService {
-    private static final String TAG = "ContactsIndexerMaintena";
-
-    /**
-     * Generate job ids in the range (MIN_INDEXER_JOB_ID, MAX_INDEXER_JOB_ID) to avoid conflicts
-     * with other jobs scheduled by the system service. The range corresponds to 21475 job ids,
-     * which is the maximum number of user ids in the system.
-     *
-     * @see com.android.server.pm.UserManagerService#MAX_USER_ID
-     */
-    public static final int MIN_INDEXER_JOB_ID = 16942831; // corresponds to ag/16942831
-    private static final int MAX_INDEXER_JOB_ID = 16964306; // 16942831 + 21475
-
-    private static final String EXTRA_USER_ID = "user_id";
-
-    private static final Executor EXECUTOR = new ThreadPoolExecutor(/*corePoolSize=*/ 1,
-            /*maximumPoolSize=*/ 1, /*keepAliveTime=*/ 60L, TimeUnit.SECONDS,
-            new LinkedBlockingQueue<>());
-
-    /**
-     * A mapping of userId-to-CancellationSignal. Since we schedule a separate job for each user,
-     * this JobService might be executing simultaneously for the various users, so we need to keep
-     * track of the cancellation signal for each user update so we stop the appropriate update
-     * when necessary.
-     */
-    @GuardedBy("mSignals")
-    private final SparseArray<CancellationSignal> mSignals = new SparseArray<>();
-
-    /**
-     * Schedules a full update job for the given device-user.
-     *
-     * @param userId Device user id for whom the full update job should be scheduled.
-     * @param periodic True to indicate that the job should be repeated.
-     * @param intervalMillis Millisecond interval for which this job should repeat.
-     */
-    static void scheduleFullUpdateJob(Context context, @UserIdInt int userId,
-            boolean periodic, long intervalMillis) {
-        int jobId = getJobIdForUser(userId);
-        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
-        ComponentName component =
-                new ComponentName(context, ContactsIndexerMaintenanceService.class);
-        final PersistableBundle extras = new PersistableBundle();
-        extras.putInt(EXTRA_USER_ID, userId);
-        JobInfo.Builder jobInfoBuilder =
-                new JobInfo.Builder(jobId, component)
-                        .setExtras(extras)
-                        .setRequiresBatteryNotLow(true)
-                        .setRequiresDeviceIdle(true)
-                        .setPersisted(true);
-
-        if (periodic) {
-            // Specify a flex value of 1/2 the interval so that the job is scheduled to run
-            // in the [interval/2, interval) time window, assuming the other conditions are
-            // met. This avoids the scenario where the next full-update job is started within
-            // a short duration of the previous run.
-            jobInfoBuilder.setPeriodic(intervalMillis, /*flexMillis=*/ intervalMillis/2);
-        }
-        JobInfo jobInfo = jobInfoBuilder.build();
-        JobInfo pendingJobInfo = jobScheduler.getPendingJob(jobId);
-        // Don't reschedule a pending job if the parameters haven't changed.
-        if (jobInfo.equals(pendingJobInfo)) {
-            return;
-        }
-        jobScheduler.schedule(jobInfo);
-        if (LogUtil.DEBUG) {
-            Log.v(TAG, "Scheduled full update job " + jobId + " for user " + userId);
-        }
-    }
-
-    /**
-     * Cancel full update job for the given user.
-     *
-     * @param userId The user id for whom the full update job needs to be cancelled.
-     */
-    private static void cancelFullUpdateJob(@NonNull Context context, @UserIdInt int userId) {
-        Objects.requireNonNull(context);
-        int jobId = getJobIdForUser(userId);
-        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
-        jobScheduler.cancel(jobId);
-        if (LogUtil.DEBUG) {
-            Log.v(TAG, "Canceled full update job " + jobId + " for user " + userId);
-        }
-    }
-
-    /**
-     * Check if a full update job is scheduled for the given user.
-     *
-     * @param userId The user id for whom the check for scheduled job needs to be performed
-     *
-     * @return true if a scheduled job exists
-     */
-    public static boolean isFullUpdateJobScheduled(@NonNull Context context,
-            @UserIdInt int userId) {
-        Objects.requireNonNull(context);
-        int jobId = getJobIdForUser(userId);
-        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
-        return jobScheduler.getPendingJob(jobId) != null;
-    }
-
-    /**
-     * Cancel any scheduled full update job for the given user. Checks if a full update job for the
-     * given user exists before trying to cancel it.
-     *
-     * @param user The user for whom the full update job needs to be cancelled.
-     */
-    public static void cancelFullUpdateJobIfScheduled(@NonNull Context context, UserHandle user) {
-        try {
-            if (isFullUpdateJobScheduled(context, user.getIdentifier())) {
-                cancelFullUpdateJob(context, user.getIdentifier());
-            }
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Failed to cancel pending full update job ", e);
-        }
-    }
-
-    private static int getJobIdForUser(int userId) {
-        return MIN_INDEXER_JOB_ID + userId;
-    }
-
-    @Override
-    public boolean onStartJob(JobParameters params) {
-        try {
-            int userId = params.getExtras().getInt(EXTRA_USER_ID, /*defaultValue=*/ -1);
-            if (userId == -1) {
-                return false;
-            }
-
-            if (LogUtil.DEBUG) {
-                Log.v(TAG, "Full update job started for user " + userId);
-            }
-            final CancellationSignal oldSignal;
-            synchronized (mSignals) {
-                oldSignal = mSignals.get(userId);
-            }
-            if (oldSignal != null) {
-                // This could happen if we attempt to schedule a new job for the user while there's
-                // one already running.
-                Log.w(TAG, "Old update job still running for user " + userId);
-                oldSignal.cancel();
-            }
-            final CancellationSignal signal = new CancellationSignal();
-            synchronized (mSignals) {
-                mSignals.put(userId, signal);
-            }
-            EXECUTOR.execute(() -> doFullUpdateForUser(this, params, userId, signal));
-            return true;
-        } catch (RuntimeException e) {
-            Slog.wtf(TAG, "ContactsIndexerMaintenanceService.onStartJob() failed ", e);
-            return false;
-        }
-    }
-
-    /**
-     * Triggers full update from a background job for the given device-user using
-     * {@link ContactsIndexerManagerService.LocalService} manager.
-     *
-     * @param params Parameters from the job that triggered the full update.
-     * @param userId Device user id for whom the full update job should be triggered.
-     * @param signal Used to indicate if the full update task should be cancelled.
-     * @return A boolean representing whether the update operation
-     * completed or encountered an issue. This return value is only used for testing purposes.
-     */
-    @VisibleForTesting
-    @CanIgnoreReturnValue
-    protected boolean doFullUpdateForUser(Context context, JobParameters params, int userId,
-            CancellationSignal signal) {
-        try {
-            ContactsIndexerManagerService.LocalService service =
-                    LocalManagerRegistry.getManager(
-                            ContactsIndexerManagerService.LocalService.class);
-            if (service == null) {
-                Log.e(TAG, "Background job failed to trigger FullUpdate because "
-                        + "ContactsIndexerManagerService.LocalService is not available.");
-                // If a background full update job exists while ContactsIndexer is disabled, cancel
-                // the job after its first run. This will prevent any periodic jobs from being
-                // unnecessarily triggered repeatedly. If the service is null, it means the contacts
-                // indexer is disabled. So the local service is not registered during the startup.
-                cancelFullUpdateJob(context, userId);
-                return false;
-            }
-            service.doFullUpdateForUser(userId, signal);
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Background job failed to trigger FullUpdate because ", e);
-            return false;
-        } finally {
-            jobFinished(params, signal.isCanceled());
-            synchronized (mSignals) {
-                if (signal == mSignals.get(userId)) {
-                    mSignals.remove(userId);
-                }
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public boolean onStopJob(JobParameters params) {
-        try {
-            final int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue */ -1);
-            if (userId == -1) {
-                return false;
-            }
-            // This will only run on S+ builds, so no need to do a version check.
-            if (LogUtil.DEBUG) {
-                Log.d(TAG,
-                        "Stopping update job for user " + userId + " because "
-                                + params.getStopReason());
-            }
-            synchronized (mSignals) {
-                final CancellationSignal signal = mSignals.get(userId);
-                if (signal != null) {
-                    signal.cancel();
-                    mSignals.remove(userId);
-                    // We had to stop the job early. Request reschedule.
-                    return true;
-                }
-            }
-            Log.e(TAG, "JobScheduler stopped an update that wasn't happening...");
-            return false;
-        } catch (RuntimeException e) {
-            Slog.wtf(TAG, "ContactsIndexerMaintenanceService.onStopJob() failed ", e);
-            return false;
-        }
-    }
-}
+/**
+ * A copy of IndexerMaintenanceService
+ *
+ * <p>For devices T and below, we have to schedule using ContactsIndexerMaintenanceService as it has
+ * the proper permissions in core/res/AndroidManifest.xml. IndexerMaintenanceService does not have
+ * the proper permissions on T.
+ */
+public class ContactsIndexerMaintenanceService extends IndexerMaintenanceService {}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java
index 2bc8cc2..60b2e64 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerService.java
@@ -20,7 +20,7 @@
 
 import android.annotation.BinderThread;
 import android.annotation.NonNull;
-import android.annotation.UserIdInt;
+import android.app.appsearch.AppSearchEnvironment;
 import android.app.appsearch.AppSearchEnvironmentFactory;
 import android.app.appsearch.util.LogUtil;
 import android.content.BroadcastReceiver;
@@ -33,17 +33,18 @@
 import android.os.PatternMatcher;
 import android.os.UserHandle;
 import android.provider.ContactsContract;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.server.LocalManagerRegistry;
 import com.android.server.SystemService;
-import android.app.appsearch.AppSearchEnvironment;
+import com.android.server.appsearch.indexer.IndexerLocalService;
 
 import java.io.File;
 import java.io.PrintWriter;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -63,14 +64,14 @@
     private final ContactsIndexerConfig mContactsIndexerConfig;
     private final LocalService mLocalService;
     // Sparse array of ContactsIndexerUserInstance indexed by the device-user ID.
-    private final SparseArray<ContactsIndexerUserInstance> mContactsIndexersLocked =
-            new SparseArray<>();
+    private final Map<UserHandle, ContactsIndexerUserInstance> mContactsIndexersLocked =
+            new ArrayMap<>();
 
     private String mContactsProviderPackageName;
 
     /** Constructs a {@link ContactsIndexerManagerService}. */
-    public ContactsIndexerManagerService(@NonNull Context context,
-            @NonNull ContactsIndexerConfig contactsIndexerConfig) {
+    public ContactsIndexerManagerService(
+            @NonNull Context context, @NonNull ContactsIndexerConfig contactsIndexerConfig) {
         super(context);
         mContext = Objects.requireNonNull(context);
         mContactsIndexerConfig = Objects.requireNonNull(contactsIndexerConfig);
@@ -90,22 +91,22 @@
             Objects.requireNonNull(user);
             UserHandle userHandle = user.getUserHandle();
             synchronized (mContactsIndexersLocked) {
-                int userId = userHandle.getIdentifier();
-                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userId);
+                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
                 if (instance == null) {
-                    AppSearchEnvironment appSearchEnvironment = AppSearchEnvironmentFactory
-                            .getEnvironmentInstance();
-                    Context userContext = appSearchEnvironment
-                            .createContextAsUser(mContext, userHandle);
-                    File appSearchDir = appSearchEnvironment
-                            .getAppSearchDir(userContext, userHandle);
+                    AppSearchEnvironment appSearchEnvironment =
+                            AppSearchEnvironmentFactory.getEnvironmentInstance();
+                    Context userContext =
+                            appSearchEnvironment.createContextAsUser(mContext, userHandle);
+                    File appSearchDir =
+                            appSearchEnvironment.getAppSearchDir(userContext, userHandle);
                     File contactsDir = new File(appSearchDir, "contacts");
-                    instance = ContactsIndexerUserInstance.createInstance(userContext,
-                            contactsDir, mContactsIndexerConfig);
+                    instance =
+                            ContactsIndexerUserInstance.createInstance(
+                                    userContext, contactsDir, mContactsIndexerConfig);
                     if (LogUtil.DEBUG) {
                         Log.d(TAG, "Created Contacts Indexer instance for user " + userHandle);
                     }
-                    mContactsIndexersLocked.put(userId, instance);
+                    mContactsIndexersLocked.put(userHandle, instance);
                 }
                 instance.startAsync();
             }
@@ -120,10 +121,9 @@
             Objects.requireNonNull(user);
             UserHandle userHandle = user.getUserHandle();
             synchronized (mContactsIndexersLocked) {
-                int userId = userHandle.getIdentifier();
-                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userId);
+                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
                 if (instance != null) {
-                    mContactsIndexersLocked.delete(userId);
+                    mContactsIndexersLocked.remove(userHandle);
                     try {
                         instance.shutdown();
                     } catch (InterruptedException e) {
@@ -140,12 +140,11 @@
     @BinderThread
     public void dumpContactsIndexerForUser(
             @NonNull UserHandle userHandle, @NonNull PrintWriter pw, boolean verbose) {
-        try{
+        try {
             Objects.requireNonNull(userHandle);
             Objects.requireNonNull(pw);
-            int userId = userHandle.getIdentifier();
             synchronized (mContactsIndexersLocked) {
-                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userId);
+                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
                 if (instance != null) {
                     instance.dump(pw, verbose);
                 } else {
@@ -157,17 +156,18 @@
         }
     }
 
-    /**
-     * Returns the package name where the Contacts Provider is hosted.
-     */
+    /** Returns the package name where the Contacts Provider is hosted. */
     private String getContactsProviderPackageName() {
         PackageManager pm = mContext.getPackageManager();
-        List<ProviderInfo> providers = pm.queryContentProviders(/*processName=*/ null, /*uid=*/ 0,
-                PackageManager.ComponentInfoFlags.of(0));
+        List<ProviderInfo> providers =
+                pm.queryContentProviders(
+                        /* processName= */ null,
+                        /* uid= */ 0,
+                        PackageManager.ComponentInfoFlags.of(0));
         for (int i = 0; i < providers.size(); i++) {
             ProviderInfo providerInfo = providers.get(i);
             if (ContactsContract.AUTHORITY.equals(providerInfo.authority)) {
-                return  providerInfo.packageName;
+                return providerInfo.packageName;
             }
         }
         return DEFAULT_CONTACTS_PROVIDER_PACKAGE_NAME;
@@ -182,16 +182,20 @@
         contactsProviderChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
         contactsProviderChangedFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
         contactsProviderChangedFilter.addDataScheme("package");
-        contactsProviderChangedFilter.addDataSchemeSpecificPart(mContactsProviderPackageName,
-                PatternMatcher.PATTERN_LITERAL);
+        contactsProviderChangedFilter.addDataSchemeSpecificPart(
+                mContactsProviderPackageName, PatternMatcher.PATTERN_LITERAL);
         mContext.registerReceiverForAllUsers(
                 new ContactsProviderChangedReceiver(),
                 contactsProviderChangedFilter,
-                /*broadcastPermission=*/ null,
-                /*scheduler=*/ null);
+                /* broadcastPermission= */ null,
+                /* scheduler= */ null);
         if (LogUtil.DEBUG) {
-            Log.v(TAG, "Registered receiver for CP2 (package: " + mContactsProviderPackageName + ")"
-                    + " data cleared events");
+            Log.v(
+                    TAG,
+                    "Registered receiver for CP2 (package: "
+                            + mContactsProviderPackageName
+                            + ")"
+                            + " data cleared events");
         }
     }
 
@@ -232,8 +236,8 @@
                             Log.w(TAG, "uid is missing in the intent: " + intent);
                             return;
                         }
-                        int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
-                        mLocalService.doFullUpdateForUser(userId,  new CancellationSignal());
+                        mLocalService.doUpdateForUser(
+                                UserHandle.getUserHandleForUid(uid), new CancellationSignal());
                         break;
                     default:
                         Log.w(TAG, "Received unknown intent: " + intent);
@@ -244,11 +248,15 @@
         }
     }
 
-    class LocalService {
-        void doFullUpdateForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) {
+    public class LocalService implements IndexerLocalService {
+
+        /** Runs a full update for the user. */
+        @Override
+        public void doUpdateForUser(
+                @NonNull UserHandle userHandle, @NonNull CancellationSignal signal) {
             Objects.requireNonNull(signal);
             synchronized (mContactsIndexersLocked) {
-                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userId);
+                ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
                 if (instance != null) {
                     instance.doFullUpdateAsync(signal);
                 }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerPhoneNumberUtils.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerPhoneNumberUtils.java
index 3503217..41e6c07 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerPhoneNumberUtils.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerPhoneNumberUtils.java
@@ -39,8 +39,7 @@
 public class ContactsIndexerPhoneNumberUtils {
     // 3 digits international calling code and the leading "+". E.g. "+354" for Iceland.
     // So maximum 4 characters total.
-    @VisibleForTesting
-    static final int DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS = 4;
+    @VisibleForTesting static final int DIALING_CODE_WITH_PLUS_SIGN_MAX_DIGITS = 4;
     private static final String TAG = "ContactsIndexerPhoneNumberUtils";
 
     /**
@@ -56,14 +55,16 @@
      * Depending on the format original phone number is using, and the locales on the system, it may
      * not be able to produce all the variants.
      *
-     * @param resources           the application's resource
+     * @param resources the application's resource
      * @param phoneNumberOriginal the phone number in the original form from CP2.
      * @param phoneNumberFromCP2InE164 the phone number in e164 from {@link Phone#NORMALIZED_NUMBER}
      * @return a set containing different phone variants created.
      */
     @NonNull
-    public static Set<String> createPhoneNumberVariants(@NonNull Resources resources,
-            @NonNull String phoneNumberOriginal, @Nullable String phoneNumberFromCP2InE164) {
+    public static Set<String> createPhoneNumberVariants(
+            @NonNull Resources resources,
+            @NonNull String phoneNumberOriginal,
+            @Nullable String phoneNumberFromCP2InE164) {
         Objects.requireNonNull(resources);
         Objects.requireNonNull(phoneNumberOriginal);
 
@@ -109,8 +110,8 @@
             String phoneNumberNormalizedWithoutCountryCode = result.second;
             phoneNumberVariants.add(phoneNumberNormalizedWithoutCountryCode);
             // create phone number in national format, and generate variants based on it.
-            String nationalFormat = createFormatNational(phoneNumberNormalizedWithoutCountryCode,
-                    isoCountryCode);
+            String nationalFormat =
+                    createFormatNational(phoneNumberNormalizedWithoutCountryCode, isoCountryCode);
             // lastly, we want to index a national format with a country dialing code:
             // E.g. for (202) 555-0111, we also want to index "1 (202) 555-0111". So when the query
             // is "1 202" or "1 (202)", a match can still be returned.
@@ -133,8 +134,8 @@
      * Parses a phone number in e164 format.
      *
      * @return a pair of dialing code and a normalized phone number without the dialing code. E.g.
-     * for +12025550111, this function returns "+1" and "2025550111". {@code null} if phone number
-     * is not in a valid e164 form.
+     *     for +12025550111, this function returns "+1" and "2025550111". {@code null} if phone
+     *     number is not in a valid e164 form.
      */
     @Nullable
     static Pair<String, String> parsePhoneNumberInE164(@NonNull String phoneNumberInE164) {
@@ -163,16 +164,16 @@
      * country code "US".
      *
      * @param phoneNumberNormalized normalized number. E.g. for phone number 202-555-0111, its
-     *                              normalized form would be 2025550111.
-     * @param countryCode           the country code to be used to format the phone number. If it is
-     *                              {@code null}, it will try the country codes from the locales in
-     *                              the configuration and return the first match.
+     *     normalized form would be 2025550111.
+     * @param countryCode the country code to be used to format the phone number. If it is {@code
+     *     null}, it will try the country codes from the locales in the configuration and return the
+     *     first match.
      * @return the national format of the phone number. {@code null} if {@code countryCode} is
-     * {@code null}.
+     *     {@code null}.
      */
     @Nullable
-    static String createFormatNational(@NonNull String phoneNumberNormalized,
-            @Nullable String countryCode) {
+    static String createFormatNational(
+            @NonNull String phoneNumberNormalized, @Nullable String countryCode) {
         Objects.requireNonNull(phoneNumberNormalized);
 
         if (TextUtils.isEmpty(countryCode)) {
@@ -182,8 +183,7 @@
     }
 
     /**
-     * Adds the variants generated from the phone number in national format into the given
-     * set.
+     * Adds the variants generated from the phone number in national format into the given set.
      *
      * <p>E.g. for national format (202) 555-0111, we will add itself as a variant, as well as (202)
      * 5550111 by removing the hyphen(last non-digit character).
@@ -191,8 +191,8 @@
      * @param phoneNumberNational phone number in national format. E.g. (202)-555-0111
      * @param phoneNumberVariants set to hold the generated variants.
      */
-    static void addVariantsFromFormatNational(@Nullable String phoneNumberNational,
-            @NonNull Set<String> phoneNumberVariants) {
+    static void addVariantsFromFormatNational(
+            @Nullable String phoneNumberNational, @NonNull Set<String> phoneNumberVariants) {
         Objects.requireNonNull(phoneNumberVariants);
 
         if (TextUtils.isEmpty(phoneNumberNational)) {
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerSettings.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerSettings.java
index 15107b2..b52bee6 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerSettings.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerSettings.java
@@ -30,14 +30,16 @@
 
 /**
  * Contacts indexer settings backed by a PersistableBundle.
- * <p>
- * Holds settings such as:
+ *
+ * <p>Holds settings such as:
+ *
  * <ul>
- * <li>the last time a full update was performed
- * <li>the last time a delta update was performed
- * <li>the time of the last CP2 contact update
- * <li>the time of the last CP2 contact deletion
+ *   <li>the last time a full update was performed
+ *   <li>the last time a delta update was performed
+ *   <li>the time of the last CP2 contact update
+ *   <li>the time of the last CP2 contact deletion
  * </ul>
+ *
  * <p>This class is NOT thread safe (similar to {@link PersistableBundle} which it wraps).
  *
  * @hide
@@ -73,58 +75,42 @@
         writeBundle(mFile, mBundle);
     }
 
-    /**
-     * Returns the timestamp of when the last full update occurred in milliseconds.
-     */
+    /** Returns the timestamp of when the last full update occurred in milliseconds. */
     public long getLastFullUpdateTimestampMillis() {
         return mBundle.getLong(LAST_FULL_UPDATE_TIMESTAMP_KEY);
     }
 
-    /**
-     * Sets the timestamp of when the last full update occurred in milliseconds.
-     */
+    /** Sets the timestamp of when the last full update occurred in milliseconds. */
     public void setLastFullUpdateTimestampMillis(long timestampMillis) {
         mBundle.putLong(LAST_FULL_UPDATE_TIMESTAMP_KEY, timestampMillis);
     }
 
-    /**
-     * Returns the timestamp of when the last delta update occurred in milliseconds.
-     */
+    /** Returns the timestamp of when the last delta update occurred in milliseconds. */
     public long getLastDeltaUpdateTimestampMillis() {
         return mBundle.getLong(LAST_DELTA_UPDATE_TIMESTAMP_KEY);
     }
 
-    /**
-     * Sets the timestamp of when the last delta update occurred in milliseconds.
-     */
+    /** Sets the timestamp of when the last delta update occurred in milliseconds. */
     public void setLastDeltaUpdateTimestampMillis(long timestampMillis) {
         mBundle.putLong(LAST_DELTA_UPDATE_TIMESTAMP_KEY, timestampMillis);
     }
 
-    /**
-     * Returns the timestamp of when the last contact in CP2 was updated in milliseconds.
-     */
+    /** Returns the timestamp of when the last contact in CP2 was updated in milliseconds. */
     public long getLastContactUpdateTimestampMillis() {
         return mBundle.getLong(LAST_CONTACT_UPDATE_TIMESTAMP_KEY);
     }
 
-    /**
-     * Sets the timestamp of when the last contact in CP2 was updated in milliseconds.
-     */
+    /** Sets the timestamp of when the last contact in CP2 was updated in milliseconds. */
     public void setLastContactUpdateTimestampMillis(long timestampMillis) {
         mBundle.putLong(LAST_CONTACT_UPDATE_TIMESTAMP_KEY, timestampMillis);
     }
 
-    /**
-     * Returns the timestamp of when the last contact in CP2 was deleted in milliseconds.
-     */
+    /** Returns the timestamp of when the last contact in CP2 was deleted in milliseconds. */
     public long getLastContactDeleteTimestampMillis() {
         return mBundle.getLong(LAST_CONTACT_DELETE_TIMESTAMP_KEY);
     }
 
-    /**
-     * Sets the timestamp of when the last contact in CP2 was deleted in milliseconds.
-     */
+    /** Sets the timestamp of when the last contact in CP2 was deleted in milliseconds. */
     public void setLastContactDeleteTimestampMillis(long timestampMillis) {
         mBundle.putLong(LAST_CONTACT_DELETE_TIMESTAMP_KEY, timestampMillis);
     }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java
index 47da1fe..443d46e 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstance.java
@@ -16,6 +16,8 @@
 
 package com.android.server.appsearch.contactsindexer;
 
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.CONTACTS_INDEXER;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.appsearch.AppSearchEnvironmentFactory;
@@ -32,6 +34,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
 import com.android.server.appsearch.stats.AppSearchStatsLog;
 
 import java.io.File;
@@ -68,10 +71,12 @@
     // Those two booleans below are used for batching/throttling the contact change
     // notification so we won't schedule too many delta updates.
     private final Object mDeltaUpdateLock = new Object();
+
     // Whether a delta update has been scheduled or run. Now we only allow one delta update being
     // run at a time.
     @GuardedBy("mDeltaUpdateLock")
     private boolean mDeltaUpdateScheduled = false;
+
     // Whether we are receiving notifications from CP2.
     @GuardedBy("mDeltaUpdateLock")
     private boolean mCp2ChangePending = false;
@@ -87,8 +92,8 @@
      * executor is shutdown during {@link #shutdown()}.
      *
      * <p>Note that this executor is used as both work and callback executors by {@link
-     * #mAppSearchHelper} which is fine because AppSearch should be able to handle exceptions
-     * thrown by them.
+     * #mAppSearchHelper} which is fine because AppSearch should be able to handle exceptions thrown
+     * by them.
      */
     private final ExecutorService mSingleThreadedExecutor;
 
@@ -100,32 +105,42 @@
      * @param contactsDir data directory for ContactsIndexer.
      */
     @NonNull
-    public static ContactsIndexerUserInstance createInstance(@NonNull Context userContext,
-            @NonNull File contactsDir, @NonNull ContactsIndexerConfig contactsIndexerConfig) {
+    public static ContactsIndexerUserInstance createInstance(
+            @NonNull Context userContext,
+            @NonNull File contactsDir,
+            @NonNull ContactsIndexerConfig contactsIndexerConfig) {
         Objects.requireNonNull(userContext);
         Objects.requireNonNull(contactsDir);
         Objects.requireNonNull(contactsIndexerConfig);
 
-        ExecutorService singleThreadedExecutor = AppSearchEnvironmentFactory
-                .getEnvironmentInstance().createSingleThreadExecutor();
-        return createInstance(userContext, contactsDir, contactsIndexerConfig,
-                singleThreadedExecutor);
+        ExecutorService singleThreadedExecutor =
+                AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor();
+        return createInstance(
+                userContext, contactsDir, contactsIndexerConfig, singleThreadedExecutor);
     }
 
     @VisibleForTesting
     @NonNull
-    /*package*/ static ContactsIndexerUserInstance createInstance(@NonNull Context context,
-            @NonNull File contactsDir, @NonNull ContactsIndexerConfig contactsIndexerConfig,
+    /*package*/ static ContactsIndexerUserInstance createInstance(
+            @NonNull Context context,
+            @NonNull File contactsDir,
+            @NonNull ContactsIndexerConfig contactsIndexerConfig,
             @NonNull ExecutorService executorService) {
         Objects.requireNonNull(context);
         Objects.requireNonNull(contactsDir);
         Objects.requireNonNull(contactsIndexerConfig);
         Objects.requireNonNull(executorService);
 
-        AppSearchHelper appSearchHelper = AppSearchHelper.createAppSearchHelper(context,
-                executorService, contactsIndexerConfig);
-        ContactsIndexerUserInstance indexer = new ContactsIndexerUserInstance(context,
-                contactsDir, appSearchHelper, contactsIndexerConfig, executorService);
+        AppSearchHelper appSearchHelper =
+                AppSearchHelper.createAppSearchHelper(
+                        context, executorService, contactsIndexerConfig);
+        ContactsIndexerUserInstance indexer =
+                new ContactsIndexerUserInstance(
+                        context,
+                        contactsDir,
+                        appSearchHelper,
+                        contactsIndexerConfig,
+                        executorService);
         indexer.loadSettingsAsync();
 
         return indexer;
@@ -134,14 +149,15 @@
     /**
      * Constructs a {@link ContactsIndexerUserInstance}.
      *
-     * @param context                Context object passed from
-     *                               {@link ContactsIndexerManagerService}
-     * @param dataDir                data directory for storing contacts indexer state.
-     * @param contactsIndexerConfig  configuration for the Contacts Indexer.
+     * @param context Context object passed from {@link ContactsIndexerManagerService}
+     * @param dataDir data directory for storing contacts indexer state.
+     * @param contactsIndexerConfig configuration for the Contacts Indexer.
      * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure
-     *                               the thread safety of this class.
+     *     the thread safety of this class.
      */
-    private ContactsIndexerUserInstance(@NonNull Context context, @NonNull File dataDir,
+    private ContactsIndexerUserInstance(
+            @NonNull Context context,
+            @NonNull File dataDir,
             @NonNull AppSearchHelper appSearchHelper,
             @NonNull ContactsIndexerConfig contactsIndexerConfig,
             @NonNull ExecutorService singleThreadedExecutor) {
@@ -162,24 +178,30 @@
         mContext.getContentResolver()
                 .registerContentObserver(
                         ContactsContract.Contacts.CONTENT_URI,
-                        /*notifyForDescendants=*/ true,
+                        /* notifyForDescendants= */ true,
                         mContactsObserver);
 
-        executeOnSingleThreadedExecutor(() -> {
-            mAppSearchHelper.isDataLikelyWipedDuringInitAsync().thenCompose(
-                    isDataLikelyWipedDuringInit -> {
-                        if (isDataLikelyWipedDuringInit) {
-                            mSettings.reset();
-                            // Persist the settings right away just in case there is a crash later.
-                            // In this case, the full update still need to be run during the next
-                            // boot to reindex the data.
-                            persistSettings();
-                        }
-                        doCp2SyncFirstRun();
-                        // This value won't be used anymore, so return null here.
-                        return CompletableFuture.completedFuture(null);
-                    }).exceptionally(e -> Log.w(TAG, "Got exception in startAsync", e));
-        });
+        executeOnSingleThreadedExecutor(
+                () -> {
+                    mAppSearchHelper
+                            .isDataLikelyWipedDuringInitAsync()
+                            .thenCompose(
+                                    isDataLikelyWipedDuringInit -> {
+                                        if (isDataLikelyWipedDuringInit) {
+                                            mSettings.reset();
+                                            // Persist the settings right away just in case there is
+                                            // a crash later.
+                                            // In this case, the full update still need to be run
+                                            // during the next
+                                            // boot to reindex the data.
+                                            persistSettings();
+                                        }
+                                        doCp2SyncFirstRun();
+                                        // This value won't be used anymore, so return null here.
+                                        return CompletableFuture.completedFuture(null);
+                                    })
+                            .exceptionally(e -> Log.w(TAG, "Got exception in startAsync", e));
+                });
     }
 
     public void shutdown() throws InterruptedException {
@@ -188,8 +210,8 @@
         }
         mContext.getContentResolver().unregisterContentObserver(mContactsObserver);
 
-        ContactsIndexerMaintenanceService.cancelFullUpdateJobIfScheduled(mContext,
-                mContext.getUser());
+        IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                mContext, mContext.getUser(), CONTACTS_INDEXER);
         synchronized (mSingleThreadedExecutor) {
             mSingleThreadedExecutor.shutdown();
         }
@@ -198,7 +220,7 @@
 
     private class ContactsObserver extends ContentObserver {
         public ContactsObserver() {
-            super(/*handler=*/ null);
+            super(/* handler= */ null);
         }
 
         @Override
@@ -221,9 +243,9 @@
      * designed to sync only the changed contacts because the user might be actively using the
      * device at that time.
      *
-     * <p>Schedules a one-off full update job to sync all CP2 contacts when the device is idle.
-     * Also syncs a configurable number of CP2 contacts into the AppSearch Person corpus so that
-     * it's nominally functional.
+     * <p>Schedules a one-off full update job to sync all CP2 contacts when the device is idle. Also
+     * syncs a configurable number of CP2 contacts into the AppSearch Person corpus so that it's
+     * nominally functional.
      */
     private void doCp2SyncFirstRun() {
         // If this is not the first run of contacts indexer (lastFullUpdateTimestampMillis is not 0)
@@ -233,23 +255,33 @@
         // If the job is not scheduled but lastFullUpdateTimestampMillis is not 0, the contacts
         // indexer was disabled before. We need to reschedule the job and run a limited delta update
         // to bring latest contact change in AppSearch right away, after it is re-enabled.
-        if (mSettings.getLastFullUpdateTimestampMillis() != 0 &&
-                ContactsIndexerMaintenanceService.isFullUpdateJobScheduled(mContext,
-                        mContext.getUser().getIdentifier())) {
+        if (mSettings.getLastFullUpdateTimestampMillis() != 0
+                && IndexerMaintenanceService.isUpdateJobScheduled(
+                        mContext, mContext.getUser(), CONTACTS_INDEXER)) {
             return;
         }
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext,
-                mContext.getUser().getIdentifier(), /*periodic=*/ false, /*intervalMillis=*/ -1);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContext,
+                mContext.getUser(),
+                CONTACTS_INDEXER,
+                /* periodic= */ false,
+                /* intervalMillis= */ -1);
         // TODO(b/222126568): refactor doDeltaUpdateAsync() to return a future value of
         // ContactsUpdateStats so that it can be checked and logged here, instead of the
         // placeholder exceptionally() block that only logs to the console.
-        doDeltaUpdateAsync(mContactsIndexerConfig.getContactsFirstRunIndexingLimit(),
-                new ContactsUpdateStats()).exceptionally(t -> {
-            if (LogUtil.DEBUG) {
-                Log.d(TAG, "Failed to bootstrap Person corpus with CP2 contacts", t);
-            }
-            return null;
-        });
+        doDeltaUpdateAsync(
+                        mContactsIndexerConfig.getContactsFirstRunIndexingLimit(),
+                        new ContactsUpdateStats())
+                .exceptionally(
+                        t -> {
+                            if (LogUtil.DEBUG) {
+                                Log.d(
+                                        TAG,
+                                        "Failed to bootstrap Person corpus with CP2 contacts",
+                                        t);
+                            }
+                            return null;
+                        });
     }
 
     /**
@@ -258,13 +290,17 @@
      * @param signal Used to indicate if the full update task should be cancelled.
      */
     public void doFullUpdateAsync(@Nullable CancellationSignal signal) {
-        executeOnSingleThreadedExecutor(() -> {
-            ContactsUpdateStats updateStats = new ContactsUpdateStats();
-            doFullUpdateInternalAsync(signal, updateStats);
-            ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext,
-                    mContext.getUser().getIdentifier(), /*periodic=*/ true,
-                    mContactsIndexerConfig.getContactsFullUpdateIntervalMillis());
-        });
+        executeOnSingleThreadedExecutor(
+                () -> {
+                    ContactsUpdateStats updateStats = new ContactsUpdateStats();
+                    doFullUpdateInternalAsync(signal, updateStats);
+                    IndexerMaintenanceService.scheduleUpdateJob(
+                            mContext,
+                            mContext.getUser(),
+                            CONTACTS_INDEXER,
+                            /* periodic= */ true,
+                            mContactsIndexerConfig.getContactsFullUpdateIntervalMillis());
+                });
     }
 
     /** Dumps the internal state of this {@link ContactsIndexerUserInstance}. */
@@ -274,17 +310,17 @@
         // race condition if there is an update running while those numbers are being printed.
         // This is acceptable though for debug purpose, so still no lock here.
         pw.println(
-                "last_full_update_timestamp_millis: " +
-                        mSettings.getLastFullUpdateTimestampMillis());
+                "last_full_update_timestamp_millis: "
+                        + mSettings.getLastFullUpdateTimestampMillis());
         pw.println(
-                "last_delta_update_timestamp_millis: " +
-                        mSettings.getLastDeltaUpdateTimestampMillis());
+                "last_delta_update_timestamp_millis: "
+                        + mSettings.getLastDeltaUpdateTimestampMillis());
         pw.println(
-                "last_contact_update_timestamp_millis: " +
-                        mSettings.getLastContactUpdateTimestampMillis());
+                "last_contact_update_timestamp_millis: "
+                        + mSettings.getLastContactUpdateTimestampMillis());
         pw.println(
-                "last_contact_delete_timestamp_millis: " +
-                        mSettings.getLastContactDeleteTimestampMillis());
+                "last_contact_delete_timestamp_millis: "
+                        + mSettings.getLastContactDeleteTimestampMillis());
     }
 
     @VisibleForTesting
@@ -299,55 +335,76 @@
 
         List<String> cp2ContactIds = new ArrayList<>();
         // Get a list of all contact IDs from CP2
-        updateStats.mLastContactUpdatedTimeMillis = ContactsProviderUtil.getUpdatedContactIds(
-                mContext, /*sinceFilter=*/ 0, mContactsIndexerConfig.getContactsFullUpdateLimit(),
-                cp2ContactIds, updateStats);
+        updateStats.mLastContactUpdatedTimeMillis =
+                ContactsProviderUtil.getUpdatedContactIds(
+                        mContext,
+                        /* sinceFilter= */ 0,
+                        mContactsIndexerConfig.getContactsFullUpdateLimit(),
+                        cp2ContactIds,
+                        updateStats);
         updateStats.mPreviousLastContactUpdatedTimeMillis =
                 mSettings.getLastContactUpdateTimestampMillis();
-        return mAppSearchHelper.getAllContactIdsAsync()
-                .thenCompose(appsearchContactIds -> {
-                    // all_contacts_from_AppSearch - all_contacts_from_cp2 =
-                    // contacts_needs_to_be_removed_from_AppSearch.
-                    appsearchContactIds.removeAll(cp2ContactIds);
-                    // Full update doesn't happen very often. In normal cases, it is scheduled to
-                    // be run every 15-30 days.
-                    // One-off full update can be scheduled if
-                    // 1) during startup, full update has never been run.
-                    // 2) or we get OUT_OF_SPACE from AppSearch.
-                    // So print a message once in 15-30 days should be acceptable.
-                    Log.i(TAG, "Performing a full sync (updated:" + cp2ContactIds.size()
-                            + ", deleted:" + appsearchContactIds.size()
-                            + ") of CP2 contacts in AppSearch");
-                    return mContactsIndexerImpl.updatePersonCorpusAsync(/*wantedContactIds=*/
-                            cp2ContactIds, /*unwantedContactIds=*/ appsearchContactIds,
-                            updateStats, mContactsIndexerConfig.shouldKeepUpdatingOnError());
-                }).handle((x, t) -> {
-                    if (t != null) {
-                        Log.w(TAG, "Failed to perform full update", t);
-                        if (updateStats.mUpdateStatuses.isEmpty()
-                                && updateStats.mDeleteStatuses.isEmpty()) {
-                            // Somehow this error is not reflected in the stats, and
-                            // unfortunately we don't know what part is wrong. Just add an error
-                            // code for the update.
-                            updateStats.mUpdateStatuses.add(
-                                    ContactsUpdateStats.ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR);
-                        }
-                    }
+        return mAppSearchHelper
+                .getAllContactIdsAsync()
+                .thenCompose(
+                        appsearchContactIds -> {
+                            // all_contacts_from_AppSearch - all_contacts_from_cp2 =
+                            // contacts_needs_to_be_removed_from_AppSearch.
+                            appsearchContactIds.removeAll(cp2ContactIds);
+                            // Full update doesn't happen very often. In normal cases, it is
+                            // scheduled to
+                            // be run every 15-30 days.
+                            // One-off full update can be scheduled if
+                            // 1) during startup, full update has never been run.
+                            // 2) or we get OUT_OF_SPACE from AppSearch.
+                            // So print a message once in 15-30 days should be acceptable.
+                            if (LogUtil.INFO) {
+                                Log.i(
+                                        TAG,
+                                        "Performing a full sync (updated:"
+                                                + cp2ContactIds.size()
+                                                + ", deleted:"
+                                                + appsearchContactIds.size()
+                                                + ") of CP2 contacts in AppSearch");
+                            }
+                            return mContactsIndexerImpl.updatePersonCorpusAsync(
+                                    /* wantedContactIds= */ cp2ContactIds,
+                                    /* unwantedContactIds= */ appsearchContactIds,
+                                    updateStats,
+                                    mContactsIndexerConfig.shouldKeepUpdatingOnError());
+                        })
+                .handle(
+                        (x, t) -> {
+                            if (t != null) {
+                                Log.w(TAG, "Failed to perform full update", t);
+                                if (updateStats.mUpdateStatuses.isEmpty()
+                                        && updateStats.mDeleteStatuses.isEmpty()) {
+                                    // Somehow this error is not reflected in the stats, and
+                                    // unfortunately we don't know what part is wrong. Just add an
+                                    // error
+                                    // code for the update.
+                                    updateStats.mUpdateStatuses.add(
+                                            ContactsUpdateStats
+                                                    .ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR);
+                                }
+                            }
 
-                    // Always persist the current timestamps for full update for both success and
-                    // failure. Right now we are taking the best effort to keep CP2 and AppSearch
-                    // in sync, without any retry in case of failure. We don't want an unexpected
-                    // error, like a bad document, prevent the timestamps getting updated, which
-                    // will make the indexer fetch a lot of contacts for EACH delta update.
-                    // TODO(b/226078966) Also finding the update timestamps for last success is
-                    //  not trivial, and we should think more about how to do that correctly.
-                    mSettings.setLastFullUpdateTimestampMillis(currentTimeMillis);
-                    mSettings.setLastContactUpdateTimestampMillis(currentTimeMillis);
-                    mSettings.setLastContactDeleteTimestampMillis(currentTimeMillis);
-                    persistSettings();
-                    logStats(updateStats);
-                    return null;
-                });
+                            // Always persist the current timestamps for full update for both
+                            // success and failure. Right now we are taking the best effort to keep
+                            // CP2 and AppSearch in sync, without any retry in case of failure. We
+                            // don't want an unexpected error, like a bad document, prevent the
+                            // timestamps getting updated, which will make the indexer fetch a lot
+                            // of contacts for EACH delta update.
+                            // TODO(b/226078966) Also finding the update timestamps for last success
+                            //  is not trivial, and we should think more about how to do that
+                            //  correctly.
+                            mSettings.setLastFullUpdateTimestampMillis(currentTimeMillis);
+                            mSettings.setLastContactUpdateTimestampMillis(currentTimeMillis);
+                            mSettings.setLastContactDeleteTimestampMillis(currentTimeMillis);
+                            persistSettings();
+                            logStats(updateStats);
+                            return null;
+                        });
     }
 
     /**
@@ -391,25 +448,30 @@
             return;
         }
         mDeltaUpdateScheduled = true;
-        executeOnSingleThreadedExecutor(() -> {
-            ContactsUpdateStats updateStats = new ContactsUpdateStats();
-            // TODO(b/226489369): apply instant indexing limit on CP2 changes also?
-            // TODO(b/222126568): refactor doDeltaUpdateAsync() to return a future value of
-            //  ContactsUpdateStats so that it can be checked and logged here, instead of the
-            //  placeholder exceptionally() block that only logs to the console.
-            doDeltaUpdateAsync(mContactsIndexerConfig.getContactsDeltaUpdateLimit(),
-                    updateStats).exceptionally(t -> {
-                if (LogUtil.DEBUG) {
-                    Log.d(TAG, "Failed to index CP2 change", t);
-                }
-                return null;
-            });
-        });
+        executeOnSingleThreadedExecutor(
+                () -> {
+                    ContactsUpdateStats updateStats = new ContactsUpdateStats();
+                    // TODO(b/226489369): apply instant indexing limit on CP2 changes also?
+                    // TODO(b/222126568): refactor doDeltaUpdateAsync() to return a future value of
+                    //  ContactsUpdateStats so that it can be checked and logged here, instead of
+                    // the
+                    //  placeholder exceptionally() block that only logs to the console.
+                    doDeltaUpdateAsync(
+                                    mContactsIndexerConfig.getContactsDeltaUpdateLimit(),
+                                    updateStats)
+                            .exceptionally(
+                                    t -> {
+                                        if (LogUtil.DEBUG) {
+                                            Log.d(TAG, "Failed to index CP2 change", t);
+                                        }
+                                        return null;
+                                    });
+                });
     }
 
     /**
-     * Does the delta update. It also resets
-     * {@link ContactsIndexerUserInstance#mDeltaUpdateScheduled} to false.
+     * Does the delta update. It also resets {@link
+     * ContactsIndexerUserInstance#mDeltaUpdateScheduled} to false.
      */
     @VisibleForTesting
     /*package*/ CompletableFuture<Void> doDeltaUpdateAsync(
@@ -427,19 +489,27 @@
         long lastContactUpdateTimestampMillis = mSettings.getLastContactUpdateTimestampMillis();
         long lastContactDeleteTimestampMillis = mSettings.getLastContactDeleteTimestampMillis();
         if (LogUtil.DEBUG) {
-            Log.d(TAG, "previous timestamps --"
-                    + " lastContactUpdateTimestampMillis: " + lastContactUpdateTimestampMillis
-                    + " lastContactDeleteTimestampMillis: " + lastContactDeleteTimestampMillis);
+            Log.d(
+                    TAG,
+                    "previous timestamps --"
+                            + " lastContactUpdateTimestampMillis: "
+                            + lastContactUpdateTimestampMillis
+                            + " lastContactDeleteTimestampMillis: "
+                            + lastContactDeleteTimestampMillis);
         }
 
         List<String> wantedIds = new ArrayList<>();
         List<String> unWantedIds = new ArrayList<>();
         long mostRecentContactUpdatedTimestampMillis =
-                ContactsProviderUtil.getUpdatedContactIds(mContext,
-                        lastContactUpdateTimestampMillis, indexingLimit, wantedIds, updateStats);
+                ContactsProviderUtil.getUpdatedContactIds(
+                        mContext,
+                        lastContactUpdateTimestampMillis,
+                        indexingLimit,
+                        wantedIds,
+                        updateStats);
         long mostRecentContactDeletedTimestampMillis =
-                ContactsProviderUtil.getDeletedContactIds(mContext,
-                        lastContactDeleteTimestampMillis, unWantedIds, updateStats);
+                ContactsProviderUtil.getDeletedContactIds(
+                        mContext, lastContactDeleteTimestampMillis, unWantedIds, updateStats);
         updateStats.mLastContactUpdatedTimeMillis = mostRecentContactUpdatedTimestampMillis;
         updateStats.mLastContactDeletedTimeMillis = mostRecentContactDeletedTimestampMillis;
 
@@ -450,73 +520,86 @@
         //  timestamps for last successful deletion and update. This requires the ids from CP2
         //  are sorted in last_update_timestamp ascending order, and the code would become a
         //  little complicated.
-        return mContactsIndexerImpl.updatePersonCorpusAsync(wantedIds, unWantedIds,
-                        updateStats, mContactsIndexerConfig.shouldKeepUpdatingOnError())
-                .handle((x, t) -> {
-                    try {
-                        if (t != null) {
-                            Log.w(TAG, "Failed to perform delta update", t);
-                            if (updateStats.mUpdateStatuses.isEmpty()
-                                    && updateStats.mDeleteStatuses.isEmpty()) {
-                                // Somehow this error is not reflected in the stats, and
-                                // unfortunately we don't know which part is wrong. Just add an
-                                // error code for the update.
-                                updateStats.mUpdateStatuses.add(
-                                        ContactsUpdateStats
-                                                .ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR);
-                            }
-                        }
-                        // Persisting timestamping and logging, no matter if update succeeds or not.
-                        if (LogUtil.DEBUG) {
-                            Log.d(TAG, "updated timestamps --"
-                                    + " lastContactUpdateTimestampMillis: "
-                                    + mostRecentContactUpdatedTimestampMillis
-                                    + " lastContactDeleteTimestampMillis: "
-                                    + mostRecentContactDeletedTimestampMillis);
-                        }
-                        mSettings.setLastContactUpdateTimestampMillis(
-                                mostRecentContactUpdatedTimestampMillis);
-                        mSettings.setLastContactDeleteTimestampMillis(
-                                mostRecentContactDeletedTimestampMillis);
-                        mSettings.setLastDeltaUpdateTimestampMillis(currentTimeMillis);
-                        persistSettings();
-                        logStats(updateStats);
-                        if (updateStats.mUpdateStatuses.contains(
-                                AppSearchResult.RESULT_OUT_OF_SPACE)) {
-                            // Some indexing failed due to OUT_OF_SPACE from AppSearch. We can
-                            // simply schedule a full update so we can trim the Person corpus in
-                            // AppSearch to make some room for delta update. We need to monitor
-                            // the failure count and reasons for indexing during full update to
-                            // see if that limit (10,000) is too big right now, considering we
-                            // are sharing this limit with any AppSearch clients, e.g.
-                            // ShortcutManager, in the system server.
-                            ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext,
-                                    mContext.getUser().getIdentifier(), /*periodic=*/ false,
-                                    /*intervalMillis=*/ -1);
-                        }
+        return mContactsIndexerImpl
+                .updatePersonCorpusAsync(
+                        wantedIds,
+                        unWantedIds,
+                        updateStats,
+                        mContactsIndexerConfig.shouldKeepUpdatingOnError())
+                .handle(
+                        (x, t) -> {
+                            try {
+                                if (t != null) {
+                                    Log.w(TAG, "Failed to perform delta update", t);
+                                    if (updateStats.mUpdateStatuses.isEmpty()
+                                            && updateStats.mDeleteStatuses.isEmpty()) {
+                                        // Somehow this error is not reflected in the stats, and
+                                        // unfortunately we don't know which part is wrong. Just add
+                                        // an
+                                        // error code for the update.
+                                        updateStats.mUpdateStatuses.add(
+                                                ContactsUpdateStats
+                                                        .ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR);
+                                    }
+                                }
+                                // Persisting timestamping and logging, no matter if update succeeds
+                                // or not.
+                                if (LogUtil.DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "updated timestamps --"
+                                                    + " lastContactUpdateTimestampMillis: "
+                                                    + mostRecentContactUpdatedTimestampMillis
+                                                    + " lastContactDeleteTimestampMillis: "
+                                                    + mostRecentContactDeletedTimestampMillis);
+                                }
+                                mSettings.setLastContactUpdateTimestampMillis(
+                                        mostRecentContactUpdatedTimestampMillis);
+                                mSettings.setLastContactDeleteTimestampMillis(
+                                        mostRecentContactDeletedTimestampMillis);
+                                mSettings.setLastDeltaUpdateTimestampMillis(currentTimeMillis);
+                                persistSettings();
+                                logStats(updateStats);
+                                if (updateStats.mUpdateStatuses.contains(
+                                        AppSearchResult.RESULT_OUT_OF_SPACE)) {
+                                    // Some indexing failed due to OUT_OF_SPACE from AppSearch. We
+                                    // can simply schedule a full update so we can trim the Person
+                                    // corpus in AppSearch to make some room for delta update. We
+                                    // need to monitor the failure count and reasons for indexing
+                                    // during full update to see if that limit (10,000) is too big
+                                    // right now, considering we are sharing this limit with any
+                                    // AppSearch clients, e.g. ShortcutManager, in the system
+                                    // server.
+                                    IndexerMaintenanceService.scheduleUpdateJob(
+                                            mContext,
+                                            mContext.getUser(),
+                                            CONTACTS_INDEXER,
+                                            /* periodic= */ false,
+                                            /* intervalMillis= */ -1);
+                                }
 
-                        return null;
-                    } finally {
-                        synchronized (mDeltaUpdateLock) {
-                            // The current delta update is done. Reset the flag so new delta
-                            // update can be scheduled and run.
-                            mDeltaUpdateScheduled = false;
-                            // If another CP2 change notifications were received while this delta
-                            // update task was running, schedule it again.
-                            if (mCp2ChangePending) {
-                                scheduleDeltaUpdateLocked();
+                                return null;
+                            } finally {
+                                synchronized (mDeltaUpdateLock) {
+                                    // The current delta update is done. Reset the flag so new delta
+                                    // update can be scheduled and run.
+                                    mDeltaUpdateScheduled = false;
+                                    // If another CP2 change notifications were received while this
+                                    // delta
+                                    // update task was running, schedule it again.
+                                    if (mCp2ChangePending) {
+                                        scheduleDeltaUpdateLocked();
+                                    }
+                                }
                             }
-                        }
-                    }
-                });
+                        });
     }
 
     // Logs the stats to statsd.
     @VisibleForTesting
     void logStats(@NonNull ContactsUpdateStats updateStats) {
         int totalUpdateLatency =
-                (int) (System.currentTimeMillis()
-                        - updateStats.mUpdateAndDeleteStartTimeMillis);
+                (int) (System.currentTimeMillis() - updateStats.mUpdateAndDeleteStartTimeMillis);
         // Finalize status code for update and delete.
         if (updateStats.mUpdateStatuses.isEmpty()) {
             // SUCCESS if no error found.
@@ -532,7 +615,8 @@
         // following batches will be skipped. The contacts in those batches should be counted as
         // failure as well.
         updateStats.mContactsUpdateFailedCount =
-                updateStats.mTotalContactsToBeUpdated - updateStats.mContactsUpdateSucceededCount
+                updateStats.mTotalContactsToBeUpdated
+                        - updateStats.mContactsUpdateSucceededCount
                         - updateStats.mContactsUpdateSkippedCount;
         updateStats.mContactsDeleteFailedCount =
                 updateStats.mTotalContactsToBeDeleted - updateStats.mContactsDeleteSucceededCount;
@@ -577,17 +661,18 @@
      * timestamps persisted in the memory.
      */
     private void loadSettingsAsync() {
-        executeOnSingleThreadedExecutor(() -> {
-            boolean unused = mDataDir.mkdirs();
-            try {
-                mSettings.load();
-            } catch (IOException e) {
-                // Ignore file not found errors (bootstrap case)
-                if (!(e instanceof FileNotFoundException)) {
-                    Log.w(TAG, "Failed to load settings from disk", e);
-                }
-            }
-        });
+        executeOnSingleThreadedExecutor(
+                () -> {
+                    boolean unused = mDataDir.mkdirs();
+                    try {
+                        mSettings.load();
+                    } catch (IOException e) {
+                        // Ignore file not found errors (bootstrap case)
+                        if (!(e instanceof FileNotFoundException)) {
+                            Log.w(TAG, "Failed to load settings from disk", e);
+                        }
+                    }
+                });
     }
 
     private void persistSettings() {
@@ -599,11 +684,11 @@
     }
 
     /**
-     * Executes the given command on {@link  #mSingleThreadedExecutor} if it is still alive.
+     * Executes the given command on {@link #mSingleThreadedExecutor} if it is still alive.
      *
-     * <p>If the {@link #mSingleThreadedExecutor} has been shutdown, this method doesn't execute
-     * the given command, and returns silently. Specifically, it does not throw
-     * {@link java.util.concurrent.RejectedExecutionException}.
+     * <p>If the {@link #mSingleThreadedExecutor} has been shutdown, this method doesn't execute the
+     * given command, and returns silently. Specifically, it does not throw {@link
+     * java.util.concurrent.RejectedExecutionException}.
      *
      * @param command the runnable task
      */
@@ -618,11 +703,13 @@
                         try {
                             command.run();
                         } catch (RuntimeException e) {
-                            Slog.wtf(TAG, "ContactsIndexerUserInstance"
-                                    + ".executeOnSingleThreadedExecutor() failed ", e);
+                            Slog.wtf(
+                                    TAG,
+                                    "ContactsIndexerUserInstance"
+                                            + ".executeOnSingleThreadedExecutor() failed ",
+                                    e);
                         }
-                    }
-            );
+                    });
         }
     }
 }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java
index c129c04..2c49078 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsProviderUtil.java
@@ -44,18 +44,15 @@
     // static final string for querying CP2
     private static final String UPDATE_SINCE = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + ">?";
     private static final String UPDATE_ORDER_BY = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " DESC";
-    private static final String[] UPDATE_SELECTION = new String[]{
-            Contacts._ID,
-            Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
-    };
+    private static final String[] UPDATE_SELECTION =
+            new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP};
     private static final String DELETION_SINCE = DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">?";
-    private static final String[] DELETION_SELECTION = new String[]{
-            DeletedContacts.CONTACT_ID,
-            DeletedContacts.CONTACT_DELETED_TIMESTAMP,
-    };
+    private static final String[] DELETION_SELECTION =
+            new String[] {
+                DeletedContacts.CONTACT_ID, DeletedContacts.CONTACT_DELETED_TIMESTAMP,
+            };
 
-    private ContactsProviderUtil() {
-    }
+    private ContactsProviderUtil() {}
 
     static long getLastUpdatedTimestamp(@NonNull Cursor cursor) {
         Objects.requireNonNull(cursor);
@@ -67,16 +64,19 @@
      * Gets the ids for deleted contacts from certain timestamp.
      *
      * @param sinceFilter timestamp (milliseconds since epoch) from which ids of deleted contacts
-     *                    should be returned.
-     * @param contactIds  the Set passed in to hold the deleted contacts.
+     *     should be returned.
+     * @param contactIds the Set passed in to hold the deleted contacts.
      * @return the timestamp for the contact most recently deleted.
      */
-    static public long getDeletedContactIds(@NonNull Context context, long sinceFilter,
-            @NonNull List<String> contactIds, @Nullable ContactsUpdateStats updateStats) {
+    public static long getDeletedContactIds(
+            @NonNull Context context,
+            long sinceFilter,
+            @NonNull List<String> contactIds,
+            @Nullable ContactsUpdateStats updateStats) {
         Objects.requireNonNull(context);
         Objects.requireNonNull(contactIds);
 
-        String[] selectionArgs = new String[]{Long.toString(sinceFilter)};
+        String[] selectionArgs = new String[] {Long.toString(sinceFilter)};
         long newTimestamp = sinceFilter;
         Cursor cursor = null;
         try {
@@ -84,19 +84,18 @@
             //  LAST_DELETED_TIMESTAMP DESC. This way the 1st contact would have the last deleted
             //  timestamp.
             cursor =
-                    context.getContentResolver().query(
-                            DeletedContacts.CONTENT_URI,
-                            DELETION_SELECTION,
-                            DELETION_SINCE,
-                            selectionArgs,
-                            /*sortOrder=*/ null);
+                    context.getContentResolver()
+                            .query(
+                                    DeletedContacts.CONTENT_URI,
+                                    DELETION_SELECTION,
+                                    DELETION_SINCE,
+                                    selectionArgs,
+                                    /* sortOrder= */ null);
 
             if (cursor == null) {
-                Log.e(TAG,
-                        "Could not fetch deleted contacts - no contacts provider present?");
+                Log.e(TAG, "Could not fetch deleted contacts - no contacts provider present?");
                 if (updateStats != null) {
-                    updateStats.mDeleteStatuses.add(
-                            ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
+                    updateStats.mDeleteStatuses.add(ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
                 }
                 return newTimestamp;
             }
@@ -115,10 +114,10 @@
             if (LogUtil.DEBUG) {
                 Log.d(TAG, "Got " + rows + " deleted contacts since " + sinceFilter);
             }
-        } catch (SecurityException |
-                 SQLiteException |
-                 NullPointerException |
-                 NoClassDefFoundError e) {
+        } catch (SecurityException
+                | SQLiteException
+                | NullPointerException
+                | NoClassDefFoundError e) {
             Log.e(TAG, "ContentResolver.query failed to get latest deleted contacts.", e);
             if (updateStats != null) {
                 updateStats.mDeleteStatuses.add(
@@ -137,46 +136,54 @@
      * Returns a list of IDs, within given limit, of contacts updated since given timestamp.
      *
      * @param sinceFilter timestamp (milliseconds since epoch) from which ids of recently updated
-     *                    contacts should be returned.
-     * @param contactIds  the Set passed in to hold the recently updated contacts.
-     * @param limit       the maximum number of contacts fetched from CP2. No limit will be set if
-     *                    the value is {@link ContactsIndexerConfig#UPDATE_LIMIT_NONE}.
+     *     contacts should be returned.
+     * @param contactIds the Set passed in to hold the recently updated contacts.
+     * @param limit the maximum number of contacts fetched from CP2. No limit will be set if the
+     *     value is {@link ContactsIndexerConfig#UPDATE_LIMIT_NONE}.
      * @return the timestamp for the contact most recently updated.
      */
-    public static long getUpdatedContactIds(@NonNull Context context, long sinceFilter, int limit,
-            @NonNull List<String> contactIds, @Nullable ContactsUpdateStats updateStats) {
+    public static long getUpdatedContactIds(
+            @NonNull Context context,
+            long sinceFilter,
+            int limit,
+            @NonNull List<String> contactIds,
+            @Nullable ContactsUpdateStats updateStats) {
         Objects.requireNonNull(context);
         Objects.requireNonNull(contactIds);
 
         long newTimestamp = sinceFilter;
-        String[] selectionArgs = new String[]{Long.toString(sinceFilter)};
+        String[] selectionArgs = new String[] {Long.toString(sinceFilter)};
         // We only get the contacts from the default directory, e.g. the non-invisibles.
-        Uri.Builder contactsUriBuilder = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
-                ContactsContract.DIRECTORY_PARAM_KEY,
-                String.valueOf(ContactsContract.Directory.DEFAULT));
+        Uri.Builder contactsUriBuilder =
+                Contacts.CONTENT_URI
+                        .buildUpon()
+                        .appendQueryParameter(
+                                ContactsContract.DIRECTORY_PARAM_KEY,
+                                String.valueOf(ContactsContract.Directory.DEFAULT));
         String orderBy = null;
         if (limit >= 0) {
-            contactsUriBuilder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
-                    String.valueOf(limit));
+            contactsUriBuilder.appendQueryParameter(
+                    ContactsContract.LIMIT_PARAM_KEY, String.valueOf(limit));
             orderBy = UPDATE_ORDER_BY;
         }
-        try (Cursor cursor = context.getContentResolver().query(
-                contactsUriBuilder.build(),
-                UPDATE_SELECTION,
-                UPDATE_SINCE, selectionArgs,
-                orderBy)) {
+        try (Cursor cursor =
+                context.getContentResolver()
+                        .query(
+                                contactsUriBuilder.build(),
+                                UPDATE_SELECTION,
+                                UPDATE_SINCE,
+                                selectionArgs,
+                                orderBy)) {
             if (cursor == null) {
                 Log.w(TAG, "Failed to get a list of contacts updated since " + sinceFilter);
                 if (updateStats != null) {
-                    updateStats.mUpdateStatuses.add(
-                            ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
+                    updateStats.mUpdateStatuses.add(ContactsUpdateStats.ERROR_CODE_CP2_NULL_CURSOR);
                 }
                 return newTimestamp;
             }
 
             int contactIdIndex = cursor.getColumnIndex(Contacts._ID);
-            int timestampIndex = cursor.getColumnIndex(
-                    Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
+            int timestampIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
             int numContacts = 0;
             while (cursor.moveToNext()) {
                 // Just in case the LIMIT parameter doesn't work in the query to CP2.
@@ -193,10 +200,10 @@
             if (LogUtil.DEBUG) {
                 Log.v(TAG, "Returning " + numContacts + " updated contacts since " + sinceFilter);
             }
-        } catch (SecurityException |
-                 SQLiteException |
-                 NullPointerException |
-                 NoClassDefFoundError e) {
+        } catch (SecurityException
+                | SQLiteException
+                | NullPointerException
+                | NoClassDefFoundError e) {
             Log.e(TAG, "ContentResolver.query failed to get latest updated contacts.", e);
             // TODO(b/222126568) consider throwing an exception here. And in the caller it can
             //  still catch the exception, and based on the states(e.g. whether we query CP2
@@ -210,4 +217,4 @@
 
         return newTimestamp;
     }
-}
\ No newline at end of file
+}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java b/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java
index 4276eff..f9ca2dc 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/ContactsUpdateStats.java
@@ -29,8 +29,8 @@
 /**
  * The class to hold stats for DeltaUpdate or FullUpdate.
  *
- * <p>This will be used to populate
- * {@link AppSearchStatsLog#CONTACTS_INDEXER_UPDATE_STATS_REPORTED}.
+ * <p>This will be used to populate {@link
+ * AppSearchStatsLog#CONTACTS_INDEXER_UPDATE_STATS_REPORTED}.
  *
  * <p>This class is not thread-safe.
  *
@@ -39,33 +39,33 @@
 public class ContactsUpdateStats {
     @IntDef(
             value = {
-                    UNKNOWN_UPDATE_TYPE,
-                    DELTA_UPDATE,
-                    FULL_UPDATE,
+                UNKNOWN_UPDATE_TYPE,
+                DELTA_UPDATE,
+                FULL_UPDATE,
             })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface UpdateType {
-    }
+    public @interface UpdateType {}
 
     public static final int UNKNOWN_UPDATE_TYPE =
             AppSearchStatsLog.CONTACTS_INDEXER_UPDATE_STATS_REPORTED__UPDATE_TYPE__UNKNOWN;
+
     /** Incremental update reacting to CP2 change notifications. */
     public static final int DELTA_UPDATE =
             AppSearchStatsLog.CONTACTS_INDEXER_UPDATE_STATS_REPORTED__UPDATE_TYPE__DELTA;
+
     /** Complete update to bring AppSearch in sync with CP2. */
     public static final int FULL_UPDATE =
             AppSearchStatsLog.CONTACTS_INDEXER_UPDATE_STATS_REPORTED__UPDATE_TYPE__FULL;
 
     @IntDef(
             value = {
-                    ERROR_CODE_CP2_RUNTIME_EXCEPTION,
-                    ERROR_CODE_CP2_NULL_CURSOR,
-                    ERROR_CODE_APP_SEARCH_SYSTEM_ERROR,
-                    ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR,
+                ERROR_CODE_CP2_RUNTIME_EXCEPTION,
+                ERROR_CODE_CP2_NULL_CURSOR,
+                ERROR_CODE_APP_SEARCH_SYSTEM_ERROR,
+                ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR,
             })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface ErrorCode {
-    }
+    public @interface ErrorCode {}
 
     // Error code logged from CP2 runtime exceptions
     public static final int ERROR_CODE_CP2_RUNTIME_EXCEPTION = 10000;
@@ -77,8 +77,7 @@
     // Error code logged from ContactsIndexer for otherwise uncaught exceptions
     public static final int ERROR_CODE_CONTACTS_INDEXER_UNKNOWN_ERROR = 10200;
 
-    @UpdateType
-    int mUpdateType = UNKNOWN_UPDATE_TYPE;
+    @UpdateType int mUpdateType = UNKNOWN_UPDATE_TYPE;
     // Status for updates.
     // In case of success, we will just have one success status stored.
     // In case of Error,  we store the unique error codes during the update.
@@ -162,23 +161,41 @@
 
     @NonNull
     public String toString() {
-        return "UpdateType: " + mUpdateType
-                + ", UpdateStatus: " + mUpdateStatuses.toString()
-                + ", DeleteStatus: " + mDeleteStatuses.toString()
-                + ", UpdateAndDeleteStartTimeMillis: " + mUpdateAndDeleteStartTimeMillis
-                + ", LastFullUpdateStartTimeMillis: " + mLastFullUpdateStartTimeMillis
-                + ", LastDeltaUpdateStartTimeMillis: " + mLastDeltaUpdateStartTimeMillis
-                + ", LastContactUpdatedTimeMillis: " + mLastContactUpdatedTimeMillis
-                + ", LastContactDeletedTimeMillis: " + mLastContactDeletedTimeMillis
-                + ", PreviousLastContactUpdatedTimeMillis: " + mPreviousLastContactUpdatedTimeMillis
-                + ", ContactsUpdateFailedCount: " + mContactsUpdateFailedCount
-                + ", ContactsUpdateSucceededCount: " + mContactsUpdateSucceededCount
-                + ", NewContactsToBeUpdated: " + mNewContactsToBeUpdated
-                + ", ContactsUpdateSkippedCount: " + mContactsUpdateSkippedCount
-                + ", TotalContactsToBeUpdated: " + mTotalContactsToBeUpdated
-                + ", ContactsDeleteFailedCount: " + mContactsDeleteFailedCount
-                + ", ContactsDeleteSucceededCount: " + mContactsDeleteSucceededCount
-                + ", ContactsDeleteNotFoundCount: " + mContactsDeleteNotFoundCount
-                + ", TotalContactsToBeDeleted: " + mTotalContactsToBeDeleted;
+        return "UpdateType: "
+                + mUpdateType
+                + ", UpdateStatus: "
+                + mUpdateStatuses.toString()
+                + ", DeleteStatus: "
+                + mDeleteStatuses.toString()
+                + ", UpdateAndDeleteStartTimeMillis: "
+                + mUpdateAndDeleteStartTimeMillis
+                + ", LastFullUpdateStartTimeMillis: "
+                + mLastFullUpdateStartTimeMillis
+                + ", LastDeltaUpdateStartTimeMillis: "
+                + mLastDeltaUpdateStartTimeMillis
+                + ", LastContactUpdatedTimeMillis: "
+                + mLastContactUpdatedTimeMillis
+                + ", LastContactDeletedTimeMillis: "
+                + mLastContactDeletedTimeMillis
+                + ", PreviousLastContactUpdatedTimeMillis: "
+                + mPreviousLastContactUpdatedTimeMillis
+                + ", ContactsUpdateFailedCount: "
+                + mContactsUpdateFailedCount
+                + ", ContactsUpdateSucceededCount: "
+                + mContactsUpdateSucceededCount
+                + ", NewContactsToBeUpdated: "
+                + mNewContactsToBeUpdated
+                + ", ContactsUpdateSkippedCount: "
+                + mContactsUpdateSkippedCount
+                + ", TotalContactsToBeUpdated: "
+                + mTotalContactsToBeUpdated
+                + ", ContactsDeleteFailedCount: "
+                + mContactsDeleteFailedCount
+                + ", ContactsDeleteSucceededCount: "
+                + mContactsDeleteSucceededCount
+                + ", ContactsDeleteNotFoundCount: "
+                + mContactsDeleteNotFoundCount
+                + ", TotalContactsToBeDeleted: "
+                + mTotalContactsToBeDeleted;
     }
-}
\ No newline at end of file
+}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/CountryCodeToRegionCodeMap.java b/service/java/com/android/server/appsearch/contactsindexer/CountryCodeToRegionCodeMap.java
index 89ba21d..0301d28 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/CountryCodeToRegionCodeMap.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/CountryCodeToRegionCodeMap.java
@@ -36,8 +36,7 @@
     static Map<Integer, List<String>> getCountryCodeToRegionCodeMap() {
         // The capacity is set to 286 as there are 215 different entries,
         // and this offers a load factor of roughly 0.75.
-        Map<Integer, List<String>> countryCodeToRegionCodeMap =
-                new ArrayMap<>(286);
+        Map<Integer, List<String>> countryCodeToRegionCodeMap = new ArrayMap<>(286);
 
         ArrayList<String> listWithRegionCode;
 
@@ -942,4 +941,4 @@
 
         return countryCodeToRegionCodeMap;
     }
-}
\ No newline at end of file
+}
diff --git a/service/java/com/android/server/appsearch/contactsindexer/CountryCodeUtils.java b/service/java/com/android/server/appsearch/contactsindexer/CountryCodeUtils.java
index 97e592c..9e9309e 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/CountryCodeUtils.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/CountryCodeUtils.java
@@ -17,7 +17,6 @@
 package com.android.server.appsearch.contactsindexer;
 
 import android.util.ArrayMap;
-import android.util.ArraySet;
 
 import java.util.Collections;
 import java.util.List;
@@ -25,8 +24,7 @@
 import java.util.Set;
 
 public class CountryCodeUtils {
-    private CountryCodeUtils() {
-    }
+    private CountryCodeUtils() {}
 
     // Maps dialing code starting with "+" to its primary corresponding ISO 3166-1 alpha-2
     // country code.
diff --git a/service/java/com/android/server/appsearch/contactsindexer/FrameworkContactsIndexerConfig.java b/service/java/com/android/server/appsearch/contactsindexer/FrameworkContactsIndexerConfig.java
index a3a0adb..0ac97fc 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/FrameworkContactsIndexerConfig.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/FrameworkContactsIndexerConfig.java
@@ -36,32 +36,36 @@
     static final String KEY_CONTACTS_DELTA_UPDATE_LIMIT = "contacts_indexer_delta_update_limit";
     public static final String KEY_CONTACTS_INDEX_FIRST_MIDDLE_AND_LAST_NAMES =
             "contacts_index_first_middle_and_last_names";
-    static final String KEY_CONTACTS_KEEP_UPDATING_ON_ERROR =
-            "contacts_keep_updating_on_error";
+    static final String KEY_CONTACTS_KEEP_UPDATING_ON_ERROR = "contacts_keep_updating_on_error";
 
     @Override
     public boolean isContactsIndexerEnabled() {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_APPSEARCH,
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_CONTACTS_INDEXER_ENABLED,
                 DEFAULT_CONTACTS_INDEXER_ENABLED);
     }
 
     @Override
     public int getContactsFirstRunIndexingLimit() {
-        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_APPSEARCH,
-                KEY_CONTACTS_INSTANT_INDEXING_LIMIT, DEFAULT_CONTACTS_FIRST_RUN_INDEXING_LIMIT);
+        return DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_CONTACTS_INSTANT_INDEXING_LIMIT,
+                DEFAULT_CONTACTS_FIRST_RUN_INDEXING_LIMIT);
     }
 
     @Override
     public long getContactsFullUpdateIntervalMillis() {
-        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_APPSEARCH,
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_CONTACTS_FULL_UPDATE_INTERVAL_MILLIS,
                 DEFAULT_CONTACTS_FULL_UPDATE_INTERVAL_MILLIS);
     }
 
     @Override
     public int getContactsFullUpdateLimit() {
-        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_APPSEARCH,
+        return DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_CONTACTS_FULL_UPDATE_LIMIT,
                 DEFAULT_CONTACTS_FULL_UPDATE_INDEXING_LIMIT);
     }
@@ -71,21 +75,24 @@
         // TODO(b/227419499) Based on the metrics, we can tweak this number. Right now it is same
         //  as the instant indexing limit, which is 1,000. From our stats in GMSCore, 95th
         //  percentile for number of contacts on the device is around 2000 contacts.
-        return DeviceConfig.getInt(DeviceConfig.NAMESPACE_APPSEARCH,
+        return DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_CONTACTS_DELTA_UPDATE_LIMIT,
                 DEFAULT_CONTACTS_DELTA_UPDATE_INDEXING_LIMIT);
     }
 
     @Override
     public boolean shouldIndexFirstMiddleAndLastNames() {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_APPSEARCH,
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_CONTACTS_INDEX_FIRST_MIDDLE_AND_LAST_NAMES,
                 DEFAULT_CONTACTS_INDEX_FIRST_MIDDLE_AND_LAST_NAMES);
     }
 
     @Override
     public boolean shouldKeepUpdatingOnError() {
-        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_APPSEARCH,
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_CONTACTS_KEEP_UPDATING_ON_ERROR,
                 DEFAULT_CONTACTS_KEEP_UPDATING_ON_ERROR);
     }
diff --git a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
index be9ac4a..935a77e 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/PersonBuilderHelper.java
@@ -55,8 +55,8 @@
 
     // We want to store id separately even if we do have it set in the builder, since we
     // can't get its value out of the builder, which will be used to fetch fingerprints.
-    final private String mId;
-    final private Person.Builder mBuilder;
+    private final String mId;
+    private final Person.Builder mBuilder;
     private long mCreationTimestampMillis = -1;
     private Map<String, ContactPointBuilderHelper> mContactPointBuilderHelpers = new ArrayMap<>();
 
@@ -104,7 +104,8 @@
      */
     @NonNull
     public Person buildPerson() {
-        Preconditions.checkState(mCreationTimestampMillis >= 0,
+        Preconditions.checkState(
+                mCreationTimestampMillis >= 0,
                 "creationTimestamp must be explicitly set in the PersonBuilderHelper.");
 
         for (ContactPointBuilderHelper builderHelper : mContactPointBuilderHelpers.values()) {
@@ -125,15 +126,18 @@
             // This is an "a priori" document score that doesn't take any usage into account.
             // Hence, the heuristic that's used to assign the document score is to add the
             // presence or count of all the salient properties of the contact.
-            int score = BASE_SCORE + contactForFingerPrint.getContactPoints().length
-                    + contactForFingerPrint.getAdditionalNames().length;
+            int score =
+                    BASE_SCORE
+                            + contactForFingerPrint.getContactPoints().length
+                            + contactForFingerPrint.getAdditionalNames().length;
             mBuilder.setScore(score);
             mBuilder.setFingerprint(fingerprint);
             mBuilder.setCreationTimestampMillis(mCreationTimestampMillis);
         } catch (NoSuchAlgorithmException e) {
             // debug logging here to avoid flooding the log.
             if (LogUtil.DEBUG) {
-                Log.d(TAG,
+                Log.d(
+                        TAG,
                         "Failed to generate fingerprint for contact "
                                 + contactForFingerPrint.getId(),
                         e);
@@ -156,13 +160,15 @@
 
     @NonNull
     private ContactPointBuilderHelper getOrCreateContactPointBuilderHelper(@NonNull String label) {
-        ContactPointBuilderHelper builderHelper = mContactPointBuilderHelpers.get(
-                Objects.requireNonNull(label));
+        ContactPointBuilderHelper builderHelper =
+                mContactPointBuilderHelpers.get(Objects.requireNonNull(label));
         if (builderHelper == null) {
-            builderHelper = new ContactPointBuilderHelper(
-                    new ContactPoint.Builder(AppSearchHelper.NAMESPACE_NAME,
-                            /*id=*/"", // doesn't matter for this nested type.
-                            label));
+            builderHelper =
+                    new ContactPointBuilderHelper(
+                            new ContactPoint.Builder(
+                                    AppSearchHelper.NAMESPACE_NAME,
+                                    /* id= */ "", // doesn't matter for this nested type.
+                                    label));
             mContactPointBuilderHelpers.put(label, builderHelper);
         }
 
@@ -177,34 +183,38 @@
 
     @NonNull
     public PersonBuilderHelper addAppIdToPerson(@NonNull String label, @NonNull String appId) {
-        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
+        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label))
+                .mBuilder
                 .addAppId(Objects.requireNonNull(appId));
         return this;
     }
 
     public PersonBuilderHelper addEmailToPerson(@NonNull String label, @NonNull String email) {
-        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
+        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label))
+                .mBuilder
                 .addEmail(Objects.requireNonNull(email));
         return this;
     }
 
     @NonNull
     public PersonBuilderHelper addAddressToPerson(@NonNull String label, @NonNull String address) {
-        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
+        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label))
+                .mBuilder
                 .addAddress(Objects.requireNonNull(address));
         return this;
     }
 
     @NonNull
     public PersonBuilderHelper addPhoneToPerson(@NonNull String label, @NonNull String phone) {
-        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label)).mBuilder
+        getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label))
+                .mBuilder
                 .addPhone(Objects.requireNonNull(phone));
         return this;
     }
 
     @NonNull
-    public PersonBuilderHelper addPhoneVariantToPerson(@NonNull String label,
-            @NonNull String phoneVariant) {
+    public PersonBuilderHelper addPhoneVariantToPerson(
+            @NonNull String label, @NonNull String phoneVariant) {
         getOrCreateContactPointBuilderHelper(Objects.requireNonNull(label))
                 .addPhoneNumberVariant(Objects.requireNonNull(phoneVariant));
         return this;
@@ -232,12 +242,12 @@
     /**
      * Appends string representation of a {@link GenericDocument} to the {@link StringBuilder}.
      *
-     * <p>This is basically same as
-     * {@link GenericDocument#appendGenericDocumentString(IndentingStringBuilder)}, but only keep
-     * the properties part and use a normal {@link StringBuilder} to skip the indentation.
+     * <p>This is basically same as {@link
+     * GenericDocument#appendGenericDocumentString(IndentingStringBuilder)}, but only keep the
+     * properties part and use a normal {@link StringBuilder} to skip the indentation.
      */
-    private static void appendGenericDocumentString(@NonNull GenericDocument doc,
-            @NonNull StringBuilder builder) {
+    private static void appendGenericDocumentString(
+            @NonNull GenericDocument doc, @NonNull StringBuilder builder) {
         Objects.requireNonNull(doc);
         Objects.requireNonNull(builder);
 
@@ -256,12 +266,11 @@
     }
 
     /**
-     * Appends string representation of a {@link GenericDocument}'s property to the
-     * {@link StringBuilder}.
+     * Appends string representation of a {@link GenericDocument}'s property to the {@link
+     * StringBuilder}.
      *
-     * <p>This is basically same as
-     * {@link GenericDocument#appendPropertyString(String, Object, IndentingStringBuilder)}, but
-     * use a normal {@link StringBuilder} to skip the indentation.
+     * <p>This is basically same as {@link GenericDocument#appendPropertyString(String, Object,
+     * IndentingStringBuilder)}, but use a normal {@link StringBuilder} to skip the indentation.
      *
      * <p>Here we still keep most of the formatting(e.g. '\n') to make sure we won't hit some
      * possible corner cases. E.g. We will have "someProperty1: some\n Property2:..." instead of
diff --git a/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java
index cbb8986..3b016fd 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/ContactPoint.java
@@ -42,45 +42,67 @@
     public static final String CONTACT_POINT_PROPERTY_TELEPHONE = "telephone";
 
     // Schema
-    public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(
-            SCHEMA_TYPE)
-            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                    CONTACT_POINT_PROPERTY_LABEL)
-                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                    .setIndexingType(
-                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .build())
-            // appIds
-            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                    CONTACT_POINT_PROPERTY_APP_ID)
-                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                    .build())
-            // address
-            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                    CONTACT_POINT_PROPERTY_ADDRESS)
-                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                    .setIndexingType(
-                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .build())
-            // email
-            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                    CONTACT_POINT_PROPERTY_EMAIL)
-                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                    .setIndexingType(
-                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .build())
-            // telephone
-            .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                    CONTACT_POINT_PROPERTY_TELEPHONE)
-                    .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                    .setIndexingType(
-                            AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                    .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                    .build())
-            .build();
+    public static final AppSearchSchema SCHEMA =
+            new AppSearchSchema.Builder(SCHEMA_TYPE)
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            CONTACT_POINT_PROPERTY_LABEL)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build())
+                    // appIds
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            CONTACT_POINT_PROPERTY_APP_ID)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                    .build())
+                    // address
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            CONTACT_POINT_PROPERTY_ADDRESS)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build())
+                    // email
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            CONTACT_POINT_PROPERTY_EMAIL)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build())
+                    // telephone
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            CONTACT_POINT_PROPERTY_TELEPHONE)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build())
+                    .build();
 
     /** Constructs a {@link ContactPoint}. */
     @VisibleForTesting
@@ -124,9 +146,9 @@
          * Creates a new {@link Builder}
          *
          * @param namespace The namespace for this document.
-         * @param id        The id of this {@link ContactPoint}. It doesn't matter if it is used as
-         *                  a nested documents in {@link Person}.
-         * @param label     The label for this {@link ContactPoint}.
+         * @param id The id of this {@link ContactPoint}. It doesn't matter if it is used as a
+         *     nested documents in {@link Person}.
+         * @param label The label for this {@link ContactPoint}.
          */
         public Builder(@NonNull String namespace, @NonNull String id, @NonNull String label) {
             super(namespace, id, SCHEMA_TYPE);
diff --git a/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java
index cf17416..871577a 100644
--- a/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java
+++ b/service/java/com/android/server/appsearch/contactsindexer/appsearchtypes/Person.java
@@ -52,13 +52,12 @@
      */
     @IntDef(
             value = {
-                    TYPE_UNKNOWN,
-                    TYPE_NICKNAME,
-                    TYPE_PHONETIC_NAME,
+                TYPE_UNKNOWN,
+                TYPE_NICKNAME,
+                TYPE_PHONETIC_NAME,
             })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface NameType {
-    }
+    public @interface NameType {}
 
     public static final int TYPE_UNKNOWN = 0;
     public static final int TYPE_NICKNAME = 1;
@@ -82,132 +81,168 @@
     public static final String PERSON_PROPERTY_FINGERPRINT = "fingerprint";
 
     private static AppSearchSchema createSchema(boolean indexFirstMiddleAndLastNames) {
-        AppSearchSchema.Builder builder = new AppSearchSchema.Builder(SCHEMA_TYPE)
-                // full display name
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_NAME)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(
-                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build());
+        AppSearchSchema.Builder builder =
+                new AppSearchSchema.Builder(SCHEMA_TYPE)
+                        // full display name
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder(
+                                                PERSON_PROPERTY_NAME)
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build());
 
         if (indexFirstMiddleAndLastNames) {
             builder
                     // given name from CP2
-                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                            PERSON_PROPERTY_GIVEN_NAME)
-                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                            .setIndexingType(AppSearchSchema.StringPropertyConfig
-                                    .INDEXING_TYPE_PREFIXES)
-                            .setTokenizerType(AppSearchSchema.StringPropertyConfig
-                                    .TOKENIZER_TYPE_PLAIN)
-                            .build())
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            PERSON_PROPERTY_GIVEN_NAME)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build())
                     // middle name from CP2
-                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                            PERSON_PROPERTY_MIDDLE_NAME)
-                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                            .setIndexingType(AppSearchSchema.StringPropertyConfig
-                                    .INDEXING_TYPE_PREFIXES)
-                            .setTokenizerType(AppSearchSchema.StringPropertyConfig
-                                    .TOKENIZER_TYPE_PLAIN)
-                            .build())
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            PERSON_PROPERTY_MIDDLE_NAME)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build())
                     // family name from CP2
-                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                            PERSON_PROPERTY_FAMILY_NAME)
-                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                            .setIndexingType(AppSearchSchema.StringPropertyConfig
-                                    .INDEXING_TYPE_PREFIXES)
-                            .setTokenizerType(AppSearchSchema.StringPropertyConfig
-                                    .TOKENIZER_TYPE_PLAIN)
-                            .build());
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            PERSON_PROPERTY_FAMILY_NAME)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .setIndexingType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .INDEXING_TYPE_PREFIXES)
+                                    .setTokenizerType(
+                                            AppSearchSchema.StringPropertyConfig
+                                                    .TOKENIZER_TYPE_PLAIN)
+                                    .build());
         } else {
             builder
                     // given name from CP2
-                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                            PERSON_PROPERTY_GIVEN_NAME)
-                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                            .build())
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            PERSON_PROPERTY_GIVEN_NAME)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .build())
                     // middle name from CP2
-                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                            PERSON_PROPERTY_MIDDLE_NAME)
-                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                            .build())
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            PERSON_PROPERTY_MIDDLE_NAME)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .build())
                     // family name from CP2
-                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                            PERSON_PROPERTY_FAMILY_NAME)
-                            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                            .build());
+                    .addProperty(
+                            new AppSearchSchema.StringPropertyConfig.Builder(
+                                            PERSON_PROPERTY_FAMILY_NAME)
+                                    .setCardinality(
+                                            AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                    .build());
         }
 
         builder
                 // lookup uri from CP2
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                        PERSON_PROPERTY_EXTERNAL_URI)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(
+                                        PERSON_PROPERTY_EXTERNAL_URI)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
                 // corresponding name types for the names stored in additional names below.
-                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder(
-                        PERSON_PROPERTY_ADDITIONAL_NAME_TYPES)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.LongPropertyConfig.Builder(
+                                        PERSON_PROPERTY_ADDITIONAL_NAME_TYPES)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
                 // additional names e.g. nick names and phonetic names.
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                        PERSON_PROPERTY_ADDITIONAL_NAMES)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setIndexingType(
-                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(
+                                        PERSON_PROPERTY_ADDITIONAL_NAMES)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .build())
                 // isImportant. It could be used to store isStarred from CP2.
-                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
-                        PERSON_PROPERTY_IS_IMPORTANT)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                        PERSON_PROPERTY_IS_IMPORTANT)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
                 // isBot
-                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
-                        PERSON_PROPERTY_IS_BOT)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.BooleanPropertyConfig.Builder(PERSON_PROPERTY_IS_BOT)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
                 // imageUri
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                        PERSON_PROPERTY_IMAGE_URI)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_IMAGE_URI)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
                 // ContactPoint
-                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                        PERSON_PROPERTY_CONTACT_POINTS,
-                        ContactPoint.SCHEMA.getSchemaType())
-                        .setShouldIndexNestedProperties(true)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        PERSON_PROPERTY_CONTACT_POINTS,
+                                        ContactPoint.SCHEMA.getSchemaType())
+                                .setShouldIndexNestedProperties(true)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
                 // Affiliations
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                        PERSON_PROPERTY_AFFILIATIONS)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setIndexingType(
-                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(
+                                        PERSON_PROPERTY_AFFILIATIONS)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .build())
                 // Relations
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                        PERSON_PROPERTY_RELATIONS)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_RELATIONS)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
                 // Notes
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_NOTES)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setIndexingType(
-                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(PERSON_PROPERTY_NOTES)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .setIndexingType(
+                                        AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .setTokenizerType(
+                                        AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .build())
                 //
                 // Following fields are internal to ContactsIndexer.
                 //
                 // Fingerprint for detecting significant changes
-                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
-                        PERSON_PROPERTY_FINGERPRINT)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build());
+                .addProperty(
+                        new AppSearchSchema.StringPropertyConfig.Builder(
+                                        PERSON_PROPERTY_FINGERPRINT)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build());
         return builder.build();
     }
 
@@ -310,9 +345,7 @@
         return contactPoints;
     }
 
-    /**
-     * Gets a byte array for the fingerprint.
-     */
+    /** Gets a byte array for the fingerprint. */
     @NonNull
     public byte[] getFingerprint() {
         return getPropertyBytes(PERSON_PROPERTY_FINGERPRINT);
@@ -320,8 +353,7 @@
 
     /** Builder for {@link Person}. */
     public static final class Builder extends GenericDocument.Builder<Builder> {
-        @NameType
-        private final List<Long> mAdditionalNameTypes = new ArrayList<>();
+        @NameType private final List<Long> mAdditionalNameTypes = new ArrayList<>();
         private final List<String> mAdditionalNames = new ArrayList<>();
         private final List<String> mAffiliations = new ArrayList<>();
         private final List<String> mRelations = new ArrayList<>();
@@ -332,8 +364,8 @@
          * Creates a new {@link ContactPoint.Builder}
          *
          * @param namespace The namespace of the Email.
-         * @param id        The ID of the Email.
-         * @param name      The name of the {@link Person}.
+         * @param id The ID of the Email.
+         * @param name The name of the {@link Person}.
          */
         public Builder(@NonNull String namespace, @NonNull String id, @NonNull String name) {
             super(namespace, id, SCHEMA_TYPE);
@@ -367,15 +399,15 @@
 
         @NonNull
         public Builder setExternalUri(@NonNull Uri externalUri) {
-            setPropertyString(PERSON_PROPERTY_EXTERNAL_URI,
-                    Objects.requireNonNull(externalUri).toString());
+            setPropertyString(
+                    PERSON_PROPERTY_EXTERNAL_URI, Objects.requireNonNull(externalUri).toString());
             return this;
         }
 
         @NonNull
         public Builder setImageUri(@NonNull Uri imageUri) {
-            setPropertyString(PERSON_PROPERTY_IMAGE_URI,
-                    Objects.requireNonNull(imageUri).toString());
+            setPropertyString(
+                    PERSON_PROPERTY_IMAGE_URI, Objects.requireNonNull(imageUri).toString());
             return this;
         }
 
@@ -433,8 +465,7 @@
          * Sets the fingerprint for this {@link Person}
          *
          * @param fingerprint byte array for the fingerprint. The size depends on the algorithm
-         *                    being used. Right now we are using md5 and generating a 16-byte
-         *                    fingerprint.
+         *     being used. Right now we are using md5 and generating a 16-byte fingerprint.
          */
         @NonNull
         public Builder setFingerprint(@NonNull byte[] fingerprint) {
@@ -444,23 +475,19 @@
 
         @NonNull
         public Person build() {
-            Preconditions.checkState(
-                    mAdditionalNameTypes.size() == mAdditionalNames.size());
+            Preconditions.checkState(mAdditionalNameTypes.size() == mAdditionalNames.size());
             long[] primitiveNameTypes = new long[mAdditionalNameTypes.size()];
             for (int i = 0; i < mAdditionalNameTypes.size(); i++) {
                 primitiveNameTypes[i] = mAdditionalNameTypes.get(i).longValue();
             }
             setPropertyLong(PERSON_PROPERTY_ADDITIONAL_NAME_TYPES, primitiveNameTypes);
-            setPropertyString(PERSON_PROPERTY_ADDITIONAL_NAMES,
-                    mAdditionalNames.toArray(new String[0]));
-            setPropertyString(PERSON_PROPERTY_AFFILIATIONS,
-                    mAffiliations.toArray(new String[0]));
-            setPropertyString(PERSON_PROPERTY_RELATIONS,
-                    mRelations.toArray(new String[0]));
-            setPropertyString(PERSON_PROPERTY_NOTES,
-                    mNotes.toArray(new String[0]));
-            setPropertyDocument(PERSON_PROPERTY_CONTACT_POINTS,
-                    mContactPoints.toArray(new ContactPoint[0]));
+            setPropertyString(
+                    PERSON_PROPERTY_ADDITIONAL_NAMES, mAdditionalNames.toArray(new String[0]));
+            setPropertyString(PERSON_PROPERTY_AFFILIATIONS, mAffiliations.toArray(new String[0]));
+            setPropertyString(PERSON_PROPERTY_RELATIONS, mRelations.toArray(new String[0]));
+            setPropertyString(PERSON_PROPERTY_NOTES, mNotes.toArray(new String[0]));
+            setPropertyDocument(
+                    PERSON_PROPERTY_CONTACT_POINTS, mContactPoints.toArray(new ContactPoint[0]));
             // TODO(b/203605504) calculate score here.
             return new Person(super.build());
         }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java b/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
index d900ecc..1d90b0a 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/AppSearchImpl.java
@@ -189,12 +189,8 @@
     @VisibleForTesting
     final IcingSearchEngine mIcingSearchEngineLocked;
 
-    // This map contains schema types and SchemaTypeConfigProtos for all package-database
-    // prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
-    // prefixed schema type to its respective SchemaTypeConfigProto.
     @GuardedBy("mReadWriteLock")
-    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMapLocked =
-            new ArrayMap<>();
+    private final SchemaCache mSchemaCacheLocked = new SchemaCache();
 
     // This map contains namespaces for all package-database prefixes. All values in the map are
     // prefixed with the package-database prefix.
@@ -383,9 +379,12 @@
                 for (int i = 0; i < schemaProtoTypesList.size(); i++) {
                     SchemaTypeConfigProto schema = schemaProtoTypesList.get(i);
                     String prefixedSchemaType = schema.getSchemaType();
-                    addToMap(mSchemaMapLocked, getPrefix(prefixedSchemaType), schema);
+                    mSchemaCacheLocked.addToSchemaMap(getPrefix(prefixedSchemaType), schema);
                 }
 
+                // Populate schema parent-to-children map
+                mSchemaCacheLocked.rebuildSchemaParentToChildrenMap();
+
                 // Populate namespace map
                 List<String> prefixedNamespaceList =
                         getAllNamespacesResultProto.getNamespacesList();
@@ -559,7 +558,7 @@
                         databaseName,
                         // A CallerAccess object for internal use that has local access to this
                         // database.
-                        new CallerAccess(/*callingPackageName=*/ packageName));
+                        new CallerAccess(/* callingPackageName= */ packageName));
         long getOldSchemaEndTimeMillis = SystemClock.elapsedRealtime();
         if (setSchemaStatsBuilder != null) {
             setSchemaStatsBuilder
@@ -697,10 +696,10 @@
 
                 if (sendNotification) {
                     mObserverManager.onSchemaChange(
-                            /*listeningPackageName=*/ listeningPackageName,
-                            /*targetPackageName=*/ packageName,
-                            /*databaseName=*/ databaseName,
-                            /*schemaName=*/ schemaName);
+                            /* listeningPackageName= */ listeningPackageName,
+                            /* targetPackageName= */ packageName,
+                            /* databaseName= */ databaseName,
+                            /* schemaName= */ schemaName);
                 }
             }
         }
@@ -804,12 +803,15 @@
         // Update derived data structures.
         for (SchemaTypeConfigProto schemaTypeConfigProto :
                 rewrittenSchemaResults.mRewrittenPrefixedTypes.values()) {
-            addToMap(mSchemaMapLocked, prefix, schemaTypeConfigProto);
+            mSchemaCacheLocked.addToSchemaMap(prefix, schemaTypeConfigProto);
         }
 
         for (String schemaType : rewrittenSchemaResults.mDeletedPrefixedTypes) {
-            removeFromMap(mSchemaMapLocked, prefix, schemaType);
+            mSchemaCacheLocked.removeFromSchemaMap(prefix, schemaType);
         }
+
+        mSchemaCacheLocked.rebuildSchemaParentToChildrenMapForPrefix(prefix);
+
         // Since the constructor of VisibilityStore will set schema. Avoid call visibility
         // store before we have already created it.
         if (mVisibilityStoreLocked != null) {
@@ -1235,7 +1237,7 @@
             removePrefixesFromDocument(documentBuilder);
             String prefix = createPrefix(packageName, databaseName);
             Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                    Objects.requireNonNull(mSchemaMapLocked.get(prefix));
+                    mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
             return GenericDocumentToProtoConverter.toGenericDocument(
                     documentBuilder.build(), prefix, schemaTypeMap, mConfig);
         } finally {
@@ -1279,7 +1281,7 @@
             // schema had ever been set for that prefix. Given we have retrieved a document from
             // the index, we know a schema had to have been set.
             Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                    Objects.requireNonNull(mSchemaMapLocked.get(prefix));
+                    mSchemaCacheLocked.getSchemaMapForPrefix(prefix);
             return GenericDocumentToProtoConverter.toGenericDocument(
                     documentBuilder.build(), prefix, schemaTypeMap, mConfig);
         } finally {
@@ -1301,6 +1303,8 @@
      */
     @NonNull
     @GuardedBy("mReadWriteLock")
+    // We only log getResultProto.toString() in fullPii trace for debugging.
+    @SuppressWarnings("LiteProtoToString")
     private DocumentProto getDocumentProtoByIdLocked(
             @NonNull String packageName,
             @NonNull String databaseName,
@@ -1399,7 +1403,7 @@
                             searchSpec,
                             Collections.singleton(prefix),
                             mNamespaceMapLocked,
-                            mSchemaMapLocked,
+                            mSchemaCacheLocked,
                             mConfig);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
@@ -1473,16 +1477,12 @@
             // SearchSpec that wants to query every visible package.
             Set<String> packageFilters = new ArraySet<>();
             if (!searchSpec.getFilterPackageNames().isEmpty()) {
-                if (searchSpec.getJoinSpec() == null) {
+                JoinSpec joinSpec = searchSpec.getJoinSpec();
+                if (joinSpec == null) {
                     packageFilters.addAll(searchSpec.getFilterPackageNames());
-                } else if (!searchSpec
-                        .getJoinSpec()
-                        .getNestedSearchSpec()
-                        .getFilterPackageNames()
-                        .isEmpty()) {
+                } else if (!joinSpec.getNestedSearchSpec().getFilterPackageNames().isEmpty()) {
                     packageFilters.addAll(searchSpec.getFilterPackageNames());
-                    packageFilters.addAll(
-                            searchSpec.getJoinSpec().getNestedSearchSpec().getFilterPackageNames());
+                    packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames());
                 }
             }
 
@@ -1508,7 +1508,7 @@
                             searchSpec,
                             prefixFilters,
                             mNamespaceMapLocked,
-                            mSchemaMapLocked,
+                            mSchemaCacheLocked,
                             mConfig);
             // Remove those inaccessible schemas.
             searchSpecToProtoConverter.removeInaccessibleSchemaFilter(
@@ -1548,7 +1548,8 @@
         long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
         SearchSpecProto finalSearchSpec = searchSpecToProtoConverter.toSearchSpecProto();
         ResultSpecProto finalResultSpec =
-                searchSpecToProtoConverter.toResultSpecProto(mNamespaceMapLocked, mSchemaMapLocked);
+                searchSpecToProtoConverter.toResultSpecProto(
+                        mNamespaceMapLocked, mSchemaCacheLocked);
         ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto();
         if (sStatsBuilder != null) {
             sStatsBuilder.setRewriteSearchSpecLatencyMillis(
@@ -1563,7 +1564,7 @@
         // Rewrite search result before we return.
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(
-                        searchResultProto, mSchemaMapLocked, mConfig);
+                        searchResultProto, mSchemaCacheLocked, mConfig);
         if (sStatsBuilder != null) {
             sStatsBuilder.setRewriteSearchResultLatencyMillis(
                     (int) (SystemClock.elapsedRealtime() - rewriteSearchResultLatencyStartMillis));
@@ -1572,6 +1573,8 @@
     }
 
     @GuardedBy("mReadWriteLock")
+    // We only log searchSpec, scoringSpec and resultSpec in fullPii trace for debugging.
+    @SuppressWarnings("LiteProtoToString")
     private SearchResultProto searchInIcingLocked(
             @NonNull SearchSpecProto searchSpec,
             @NonNull ResultSpecProto resultSpec,
@@ -1646,7 +1649,7 @@
                             searchSuggestionSpec,
                             Collections.singleton(prefix),
                             mNamespaceMapLocked,
-                            mSchemaMapLocked);
+                            mSchemaCacheLocked);
 
             if (searchSuggestionSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return an
@@ -1683,7 +1686,7 @@
         mReadWriteLock.readLock().lock();
         try {
             Map<String, Set<String>> packageToDatabases = new ArrayMap<>();
-            for (String prefix : mSchemaMapLocked.keySet()) {
+            for (String prefix : mSchemaCacheLocked.getAllPrefixes()) {
                 String packageName = getPackageName(prefix);
 
                 Set<String> databases = packageToDatabases.get(packageName);
@@ -1767,7 +1770,7 @@
             // Rewrite search result before we return.
             SearchResultPage searchResultPage =
                     SearchResultToProtoConverter.toSearchResultPage(
-                            searchResultProto, mSchemaMapLocked, mConfig);
+                            searchResultProto, mSchemaCacheLocked, mConfig);
             if (sStatsBuilder != null) {
                 sStatsBuilder.setRewriteSearchResultLatencyMillis(
                         (int)
@@ -1923,7 +1926,7 @@
             checkSuccess(deleteResultProto.getStatus());
 
             // Update derived maps
-            updateDocumentCountAfterRemovalLocked(packageName, /*numDocumentsDeleted=*/ 1);
+            updateDocumentCountAfterRemovalLocked(packageName, /* numDocumentsDeleted= */ 1);
 
             // Prepare notifications
             if (schemaType != null) {
@@ -1998,7 +2001,7 @@
                             searchSpec,
                             Collections.singleton(prefix),
                             mNamespaceMapLocked,
-                            mSchemaMapLocked,
+                            mSchemaCacheLocked,
                             mConfig);
             if (searchSpecToProtoConverter.hasNothingToSearch()) {
                 // there is nothing to search over given their search filters, so we can return
@@ -2399,7 +2402,7 @@
                     finalSchema);
             SetSchemaResultProto setSchemaResultProto =
                     mIcingSearchEngineLocked.setSchema(
-                            finalSchema, /*ignoreErrorsAndDeleteDocuments=*/ true);
+                            finalSchema, /* ignoreErrorsAndDeleteDocuments= */ true);
             LogUtil.piiTrace(
                     TAG,
                     "clearPackageData.setSchema, response",
@@ -2420,10 +2423,9 @@
                     }
                     for (String databaseName : databaseNames) {
                         String removedPrefix = createPrefix(packageName, databaseName);
-                        Map<String, SchemaTypeConfigProto> removedSchemas =
-                                Objects.requireNonNull(mSchemaMapLocked.remove(removedPrefix));
+                        Set<String> removedSchemas = mSchemaCacheLocked.removePrefix(removedPrefix);
                         if (mVisibilityStoreLocked != null) {
-                            mVisibilityStoreLocked.removeVisibility(removedSchemas.keySet());
+                            mVisibilityStoreLocked.removeVisibility(removedSchemas);
                         }
 
                         mNamespaceMapLocked.remove(removedPrefix);
@@ -2453,7 +2455,7 @@
                 resetResultProto.getStatus(),
                 resetResultProto);
         mOptimizeIntervalCountLocked = 0;
-        mSchemaMapLocked.clear();
+        mSchemaCacheLocked.clear();
         mNamespaceMapLocked.clear();
         mDocumentCountMapLocked.clear();
         synchronized (mNextPageTokensLocked) {
@@ -2702,26 +2704,6 @@
         values.add(prefixedValue);
     }
 
-    private static void addToMap(
-            Map<String, Map<String, SchemaTypeConfigProto>> map,
-            String prefix,
-            SchemaTypeConfigProto schemaTypeConfigProto) {
-        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
-        if (schemaTypeMap == null) {
-            schemaTypeMap = new ArrayMap<>();
-            map.put(prefix, schemaTypeMap);
-        }
-        schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
-    }
-
-    private static void removeFromMap(
-            Map<String, Map<String, SchemaTypeConfigProto>> map, String prefix, String schemaType) {
-        Map<String, SchemaTypeConfigProto> schemaTypeMap = map.get(prefix);
-        if (schemaTypeMap != null) {
-            schemaTypeMap.remove(schemaType);
-        }
-    }
-
     /**
      * Checks the given status code and throws an {@link AppSearchException} if code is an error.
      *
@@ -2846,16 +2828,20 @@
             if (Log.isLoggable(icingTag, Log.VERBOSE)) {
                 boolean unused =
                         IcingSearchEngine.setLoggingLevel(
-                                LogSeverity.Code.VERBOSE, /*verbosity=*/ (short) 1);
+                                LogSeverity.Code.VERBOSE, /* verbosity= */ (short) 1);
                 return;
             } else if (Log.isLoggable(icingTag, Log.DEBUG)) {
-                IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);
+                boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.DBG);
                 return;
             }
         }
-        if (Log.isLoggable(icingTag, Log.INFO)) {
-            boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);
-        } else if (Log.isLoggable(icingTag, Log.WARN)) {
+        if (LogUtil.INFO) {
+            if (Log.isLoggable(icingTag, Log.INFO)) {
+                boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO);
+                return;
+            }
+        }
+        if (Log.isLoggable(icingTag, Log.WARN)) {
             boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING);
         } else if (Log.isLoggable(icingTag, Log.ERROR)) {
             boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR);
@@ -2882,11 +2868,7 @@
     public List<String> getAllPrefixedSchemaTypes() {
         mReadWriteLock.readLock().lock();
         try {
-            List<String> cachedPrefixedSchemaTypes = new ArrayList<>();
-            for (Map<String, SchemaTypeConfigProto> value : mSchemaMapLocked.values()) {
-                cachedPrefixedSchemaTypes.addAll(value.keySet());
-            }
-            return cachedPrefixedSchemaTypes;
+            return mSchemaCacheLocked.getAllPrefixedSchemaTypes();
         } finally {
             mReadWriteLock.readLock().unlock();
         }
@@ -2901,8 +2883,8 @@
      *     code.
      * @return {@link AppSearchResult} error code
      */
-    private static @AppSearchResult.ResultCode int statusProtoToResultCode(
-            @NonNull StatusProto statusProto) {
+    @AppSearchResult.ResultCode
+    private static int statusProtoToResultCode(@NonNull StatusProto statusProto) {
         return ResultCodeToProtoConverter.toResultCode(statusProto.getCode());
     }
 }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/AppSearchLogger.java b/service/java/com/android/server/appsearch/external/localstorage/AppSearchLogger.java
index 88af975..32b20e9 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/AppSearchLogger.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/AppSearchLogger.java
@@ -24,14 +24,17 @@
 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats;
 import com.android.server.appsearch.external.localstorage.stats.RemoveStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchSessionStats;
 import com.android.server.appsearch.external.localstorage.stats.SearchStats;
 import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
 
+import java.util.List;
+
 /**
  * An interface for implementing client-defined logging AppSearch operations stats.
  *
  * <p>Any implementation needs to provide general information on how to log all the stats types.
- * (e.g. {@link CallStats})
+ * (for example {@link CallStats})
  *
  * <p>All implementations of this interface must be thread safe.
  *
@@ -62,5 +65,26 @@
     /** Logs {@link SchemaMigrationStats} */
     void logStats(@NonNull SchemaMigrationStats stats);
 
+    /**
+     * Logs a list of {@link SearchSessionStats}.
+     *
+     * <p>Since the client app may report search intents belonging to different search sessions in a
+     * single taken action reporting request, the stats extractor will separate them into multiple
+     * search sessions. Therefore, we need a list of {@link SearchSessionStats} here.
+     *
+     * <p>For example, the client app reports the following search intent sequence:
+     *
+     * <ul>
+     *   <li>t = 1, the user searches "a" with some clicks.
+     *   <li>t = 5, the user searches "app" with some clicks.
+     *   <li>t = 10000, the user searches "email" with some clicks.
+     * </ul>
+     *
+     * The extractor will detect "email" belongs to a completely independent search session, and
+     * creates 2 {@link SearchSessionStats} with search intents ["a", "app"] and ["email"]
+     * respectively.
+     */
+    void logStats(@NonNull List<SearchSessionStats> searchSessionsStats);
+
     // TODO(b/173532925) Add remaining logStats once we add all the stats.
 }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/AppSearchLoggerHelper.java b/service/java/com/android/server/appsearch/external/localstorage/AppSearchLoggerHelper.java
index 3aab44f..58f8599 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/AppSearchLoggerHelper.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/AppSearchLoggerHelper.java
@@ -165,11 +165,9 @@
         Objects.requireNonNull(fromNativeStats);
         Objects.requireNonNull(toStatsBuilder);
 
-        @SuppressWarnings("deprecation")
-        int deleteType = DeleteStatsProto.DeleteType.Code.DEPRECATED_QUERY.getNumber();
         toStatsBuilder
                 .setNativeLatencyMillis(fromNativeStats.getLatencyMs())
-                .setDeleteType(deleteType)
+                .setDeleteType(RemoveStats.QUERY)
                 .setDeletedDocumentCount(fromNativeStats.getNumDocumentsDeleted());
     }
 
diff --git a/service/java/com/android/server/appsearch/external/localstorage/IcingOptionsConfig.java b/service/java/com/android/server/appsearch/external/localstorage/IcingOptionsConfig.java
index e2b569b..f05bc43 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/IcingOptionsConfig.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/IcingOptionsConfig.java
@@ -33,7 +33,7 @@
 
     boolean DEFAULT_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT = false;
 
-    float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.0f;
+    float DEFAULT_OPTIMIZE_REBUILD_INDEX_THRESHOLD = 0.9f;
 
     /**
      * The default compression level in IcingSearchEngineOptions proto matches the
@@ -60,7 +60,7 @@
      */
     int DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD = 65536;
 
-    boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = false;
+    boolean DEFAULT_LITE_INDEX_SORT_AT_INDEXING = true;
 
     /**
      * The default sort threshold for the lite index when sort at indexing is enabled. 8192 is
diff --git a/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java b/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java
index 4b711ae..6b22a53 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/ObserverManager.java
@@ -22,6 +22,7 @@
 import android.app.appsearch.observer.ObserverCallback;
 import android.app.appsearch.observer.ObserverSpec;
 import android.app.appsearch.observer.SchemaChangeInfo;
+import android.app.appsearch.util.ExceptionUtil;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -71,8 +72,12 @@
 
         @Override
         public boolean equals(@Nullable Object o) {
-            if (this == o) return true;
-            if (!(o instanceof DocumentChangeGroupKey)) return false;
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof DocumentChangeGroupKey)) {
+                return false;
+            }
             DocumentChangeGroupKey that = (DocumentChangeGroupKey) o;
             return mPackageName.equals(that.mPackageName)
                     && mDatabaseName.equals(that.mDatabaseName)
@@ -215,9 +220,9 @@
                     continue; // Observer doesn't want this notification
                 }
                 if (!VisibilityUtil.isSchemaSearchableByCaller(
-                        /*callerAccess=*/ observerInfo.mListeningPackageAccess,
-                        /*targetPackageName=*/ packageName,
-                        /*prefixedSchema=*/ prefixedSchema,
+                        /* callerAccess= */ observerInfo.mListeningPackageAccess,
+                        /* targetPackageName= */ packageName,
+                        /* prefixedSchema= */ prefixedSchema,
                         visibilityStore,
                         visibilityChecker)) {
                     continue; // Observer can't have this notification.
@@ -344,9 +349,9 @@
                     continue; // Observer doesn't want this notification
                 }
                 if (!VisibilityUtil.isSchemaSearchableByCaller(
-                        /*callerAccess=*/ observerInfo.mListeningPackageAccess,
-                        /*targetPackageName=*/ packageName,
-                        /*prefixedSchema=*/ prefixedSchema,
+                        /* callerAccess= */ observerInfo.mListeningPackageAccess,
+                        /* targetPackageName= */ packageName,
+                        /* prefixedSchema= */ prefixedSchema,
                         visibilityStore,
                         visibilityChecker)) {
                     continue; // Observer can't have this notification.
@@ -403,16 +408,17 @@
                         for (Map.Entry<String, Set<String>> entry : schemaChanges.entrySet()) {
                             SchemaChangeInfo schemaChangeInfo =
                                     new SchemaChangeInfo(
-                                            /*packageName=*/ PrefixUtil.getPackageName(
+                                            /* packageName= */ PrefixUtil.getPackageName(
                                                     entry.getKey()),
-                                            /*databaseName=*/ PrefixUtil.getDatabaseName(
+                                            /* databaseName= */ PrefixUtil.getDatabaseName(
                                                     entry.getKey()),
-                                            /*changedSchemaNames=*/ entry.getValue());
+                                            /* changedSchemaNames= */ entry.getValue());
 
                             try {
                                 observerInfo.mObserverCallback.onSchemaChanged(schemaChangeInfo);
-                            } catch (Throwable t) {
-                                Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+                            } catch (RuntimeException e) {
+                                Log.w(TAG, "ObserverCallback threw exception during dispatch", e);
+                                ExceptionUtil.handleException(e);
                             }
                         }
                     }
@@ -432,8 +438,9 @@
                             try {
                                 observerInfo.mObserverCallback.onDocumentChanged(
                                         documentChangeInfo);
-                            } catch (Throwable t) {
-                                Log.w(TAG, "ObserverCallback threw exception during dispatch", t);
+                            } catch (RuntimeException e) {
+                                Log.w(TAG, "ObserverCallback threw exception during dispatch", e);
+                                ExceptionUtil.handleException(e);
                             }
                         }
                     }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/SchemaCache.java b/service/java/com/android/server/appsearch/external/localstorage/SchemaCache.java
new file mode 100644
index 0000000..ae242ea
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/SchemaCache.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Caches and manages schema information for AppSearch.
+ *
+ * @hide
+ */
+public class SchemaCache {
+    /**
+     * A map that contains schema types and SchemaTypeConfigProtos for all package-database
+     * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+     * prefixed schema type to its respective SchemaTypeConfigProto.
+     */
+    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap = new ArrayMap<>();
+
+    /**
+     * A map that contains schema types and all children schema types for all package-database
+     * prefixes. It maps each package-database prefix to an inner-map. The inner-map maps each
+     * prefixed schema type to its respective list of children prefixed schema types.
+     */
+    private final Map<String, Map<String, List<String>>> mSchemaParentToChildrenMap =
+            new ArrayMap<>();
+
+    public SchemaCache() {}
+
+    public SchemaCache(@NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+        mSchemaMap.putAll(Objects.requireNonNull(schemaMap));
+        rebuildSchemaParentToChildrenMap();
+    }
+
+    /** Returns the schema map for the given prefix. */
+    @NonNull
+    public Map<String, SchemaTypeConfigProto> getSchemaMapForPrefix(@NonNull String prefix) {
+        Objects.requireNonNull(prefix);
+
+        Map<String, SchemaTypeConfigProto> schemaMap = mSchemaMap.get(prefix);
+        if (schemaMap == null) {
+            return Collections.emptyMap();
+        }
+        return schemaMap;
+    }
+
+    /** Returns a set of all prefixes stored in the cache. */
+    @NonNull
+    public Set<String> getAllPrefixes() {
+        return Collections.unmodifiableSet(mSchemaMap.keySet());
+    }
+
+    /**
+     * Returns all prefixed schema types stored in the cache.
+     *
+     * <p>This method is inefficient to call repeatedly.
+     */
+    @NonNull
+    public List<String> getAllPrefixedSchemaTypes() {
+        List<String> cachedPrefixedSchemaTypes = new ArrayList<>();
+        for (Map<String, SchemaTypeConfigProto> value : mSchemaMap.values()) {
+            cachedPrefixedSchemaTypes.addAll(value.keySet());
+        }
+        return cachedPrefixedSchemaTypes;
+    }
+
+    /**
+     * Returns the schema types for the given set of prefixed schema types with their descendants,
+     * based on the schema parent-to-children map held in the cache.
+     */
+    @NonNull
+    public Set<String> getSchemaTypesWithDescendants(
+            @NonNull String prefix, @NonNull Set<String> prefixedSchemaTypes) {
+        Objects.requireNonNull(prefix);
+        Objects.requireNonNull(prefixedSchemaTypes);
+        Map<String, List<String>> parentToChildrenMap = mSchemaParentToChildrenMap.get(prefix);
+        if (parentToChildrenMap == null) {
+            parentToChildrenMap = Collections.emptyMap();
+        }
+
+        // Perform a BFS search on the inheritance graph started by the set of prefixedSchemaTypes.
+        Set<String> visited = new ArraySet<>();
+        Queue<String> prefixedSchemaQueue = new ArrayDeque<>(prefixedSchemaTypes);
+        while (!prefixedSchemaQueue.isEmpty()) {
+            String currentPrefixedSchema = prefixedSchemaQueue.poll();
+            if (visited.contains(currentPrefixedSchema)) {
+                continue;
+            }
+            visited.add(currentPrefixedSchema);
+            List<String> children = parentToChildrenMap.get(currentPrefixedSchema);
+            if (children == null) {
+                continue;
+            }
+            prefixedSchemaQueue.addAll(children);
+        }
+
+        return visited;
+    }
+
+    /**
+     * Rebuilds the schema parent-to-children map for the given prefix, based on the current schema
+     * map.
+     *
+     * <p>The schema parent-to-children map is required to be updated when {@link #addToSchemaMap}
+     * or {@link #removeFromSchemaMap} has been called. Otherwise, the results from {@link
+     * #getSchemaTypesWithDescendants} would be stale.
+     */
+    public void rebuildSchemaParentToChildrenMapForPrefix(@NonNull String prefix) {
+        Objects.requireNonNull(prefix);
+
+        mSchemaParentToChildrenMap.remove(prefix);
+        Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMap.get(prefix);
+        if (prefixedSchemaMap == null) {
+            return;
+        }
+
+        // Build the parent-to-children map for the current prefix.
+        Map<String, List<String>> parentToChildrenMap = new ArrayMap<>();
+        for (SchemaTypeConfigProto childSchemaConfig : prefixedSchemaMap.values()) {
+            for (int i = 0; i < childSchemaConfig.getParentTypesCount(); i++) {
+                String parent = childSchemaConfig.getParentTypes(i);
+                List<String> children = parentToChildrenMap.get(parent);
+                if (children == null) {
+                    children = new ArrayList<>();
+                    parentToChildrenMap.put(parent, children);
+                }
+                children.add(childSchemaConfig.getSchemaType());
+            }
+        }
+
+        // Record the map for the current prefix.
+        if (!parentToChildrenMap.isEmpty()) {
+            mSchemaParentToChildrenMap.put(prefix, parentToChildrenMap);
+        }
+    }
+
+    /**
+     * Rebuilds the schema parent-to-children map based on the current schema map.
+     *
+     * <p>The schema parent-to-children map is required to be updated when {@link #addToSchemaMap}
+     * or {@link #removeFromSchemaMap} has been called. Otherwise, the results from {@link
+     * #getSchemaTypesWithDescendants} would be stale.
+     */
+    public void rebuildSchemaParentToChildrenMap() {
+        mSchemaParentToChildrenMap.clear();
+        for (String prefix : mSchemaMap.keySet()) {
+            rebuildSchemaParentToChildrenMapForPrefix(prefix);
+        }
+    }
+
+    /**
+     * Adds a schema to the schema map.
+     *
+     * <p>Note that this method will invalidate the schema parent-to-children map in the cache, and
+     * either {@link #rebuildSchemaParentToChildrenMap} or {@link
+     * #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the cache.
+     */
+    public void addToSchemaMap(
+            @NonNull String prefix, @NonNull SchemaTypeConfigProto schemaTypeConfigProto) {
+        Objects.requireNonNull(prefix);
+        Objects.requireNonNull(schemaTypeConfigProto);
+
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix);
+        if (schemaTypeMap == null) {
+            schemaTypeMap = new ArrayMap<>();
+            mSchemaMap.put(prefix, schemaTypeMap);
+        }
+        schemaTypeMap.put(schemaTypeConfigProto.getSchemaType(), schemaTypeConfigProto);
+    }
+
+    /**
+     * Removes a schema from the schema map.
+     *
+     * <p>Note that this method will invalidate the schema parent-to-children map in the cache, and
+     * either {@link #rebuildSchemaParentToChildrenMap} or {@link
+     * #rebuildSchemaParentToChildrenMapForPrefix} is required to be called to update the cache.
+     */
+    public void removeFromSchemaMap(@NonNull String prefix, @NonNull String schemaType) {
+        Objects.requireNonNull(prefix);
+        Objects.requireNonNull(schemaType);
+
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = mSchemaMap.get(prefix);
+        if (schemaTypeMap != null) {
+            schemaTypeMap.remove(schemaType);
+        }
+    }
+
+    /**
+     * Removes the entry of the given prefix from both the schema map and the schema
+     * parent-to-children map, and returns the set of removed prefixed schema type.
+     */
+    @NonNull
+    public Set<String> removePrefix(@NonNull String prefix) {
+        Objects.requireNonNull(prefix);
+
+        Map<String, SchemaTypeConfigProto> removedSchemas =
+                Objects.requireNonNull(mSchemaMap.remove(prefix));
+        mSchemaParentToChildrenMap.remove(prefix);
+        return removedSchemas.keySet();
+    }
+
+    /** Clears all data in the cache. */
+    public void clear() {
+        mSchemaMap.clear();
+        mSchemaParentToChildrenMap.clear();
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverter.java
index 0eadabb..4cc5a18 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.EmbeddingVector;
 import android.app.appsearch.GenericDocument;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.util.ArrayMap;
@@ -53,6 +54,7 @@
     private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
     private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0];
     private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0];
+    private static final EmbeddingVector[] EMPTY_EMBEDDING_ARRAY = new EmbeddingVector[0];
 
     private GenericDocumentToProtoConverter() {}
 
@@ -106,6 +108,11 @@
                     DocumentProto proto = toDocumentProto(documentValues[j]);
                     propertyProto.addDocumentValues(proto);
                 }
+            } else if (property instanceof EmbeddingVector[]) {
+                EmbeddingVector[] embeddingValues = (EmbeddingVector[]) property;
+                for (int j = 0; j < embeddingValues.length; j++) {
+                    propertyProto.addVectorValues(embeddingVectorToVectorProto(embeddingValues[j]));
+                }
             } else if (property == null) {
                 throw new IllegalStateException(
                         String.format("Property \"%s\" doesn't have any value!", name));
@@ -205,6 +212,12 @@
                                     property.getDocumentValues(j), prefix, schemaTypeMap, config);
                 }
                 documentBuilder.setPropertyDocument(name, values);
+            } else if (property.getVectorValuesCount() > 0) {
+                EmbeddingVector[] values = new EmbeddingVector[property.getVectorValuesCount()];
+                for (int j = 0; j < values.length; j++) {
+                    values[j] = vectorProtoToEmbeddingVector(property.getVectorValues(j));
+                }
+                documentBuilder.setPropertyEmbedding(name, values);
             } else {
                 // TODO(b/184966497): Optimize by caching PropertyConfigProto
                 SchemaTypeConfigProto schema =
@@ -215,6 +228,33 @@
         return documentBuilder.build();
     }
 
+    /** Converts a {@link PropertyProto.VectorProto} into an {@link EmbeddingVector}. */
+    @NonNull
+    public static EmbeddingVector vectorProtoToEmbeddingVector(
+            @NonNull PropertyProto.VectorProto vectorProto) {
+        Objects.requireNonNull(vectorProto);
+
+        float[] values = new float[vectorProto.getValuesCount()];
+        for (int i = 0; i < vectorProto.getValuesCount(); i++) {
+            values[i] = vectorProto.getValues(i);
+        }
+        return new EmbeddingVector(values, vectorProto.getModelSignature());
+    }
+
+    /** Converts an {@link EmbeddingVector} into a {@link PropertyProto.VectorProto}. */
+    @NonNull
+    public static PropertyProto.VectorProto embeddingVectorToVectorProto(
+            @NonNull EmbeddingVector embedding) {
+        Objects.requireNonNull(embedding);
+
+        PropertyProto.VectorProto.Builder builder = PropertyProto.VectorProto.newBuilder();
+        for (int i = 0; i < embedding.getValues().length; i++) {
+            builder.addValues(embedding.getValues()[i]);
+        }
+        builder.setModelSignature(embedding.getModelSignature());
+        return builder.build();
+    }
+
     /**
      * Get the list of unprefixed parent type names of {@code prefixedSchemaType}.
      *
@@ -308,6 +348,9 @@
             case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
                 documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY);
                 break;
+            case AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING:
+                documentBuilder.setPropertyEmbedding(propertyName, EMPTY_EMBEDDING_ARRAY);
+                break;
             default:
                 throw new IllegalStateException("Unknown type of value: " + propertyName);
         }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/ResultCodeToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/ResultCodeToProtoConverter.java
index e340de0..e8ff775 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/ResultCodeToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/ResultCodeToProtoConverter.java
@@ -34,8 +34,8 @@
     private ResultCodeToProtoConverter() {}
 
     /** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */
-    public static @AppSearchResult.ResultCode int toResultCode(
-            @NonNull StatusProto.Code statusCode) {
+    @AppSearchResult.ResultCode
+    public static int toResultCode(@NonNull StatusProto.Code statusCode) {
         switch (statusCode) {
             case OK:
                 return AppSearchResult.RESULT_OK;
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java
index 789a3d6..e9e0b10 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverter.java
@@ -21,6 +21,7 @@
 import android.util.Log;
 
 import com.google.android.icing.proto.DocumentIndexingConfig;
+import com.google.android.icing.proto.EmbeddingIndexingConfig;
 import com.google.android.icing.proto.IntegerIndexingConfig;
 import com.google.android.icing.proto.JoinableConfig;
 import com.google.android.icing.proto.PropertyConfigProto;
@@ -54,6 +55,7 @@
         SchemaTypeConfigProto.Builder protoBuilder =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType(schema.getSchemaType())
+                        .setDescription(schema.getDescription())
                         .setVersion(version);
         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
@@ -69,7 +71,9 @@
             @NonNull AppSearchSchema.PropertyConfig property) {
         Objects.requireNonNull(property);
         PropertyConfigProto.Builder builder =
-                PropertyConfigProto.newBuilder().setPropertyName(property.getName());
+                PropertyConfigProto.newBuilder()
+                        .setPropertyName(property.getName())
+                        .setDescription(property.getDescription());
 
         // Set dataType
         @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
@@ -138,6 +142,22 @@
                                 .build();
                 builder.setIntegerIndexingConfig(integerIndexingConfig);
             }
+        } else if (property instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+            AppSearchSchema.EmbeddingPropertyConfig embeddingProperty =
+                    (AppSearchSchema.EmbeddingPropertyConfig) property;
+            // Set embedding indexing config only if it is indexable (i.e. not INDEXING_TYPE_NONE).
+            // Non-indexable embedding property only requires to builder.setDataType, without the
+            // need to set an EmbeddingIndexingConfig.
+            if (embeddingProperty.getIndexingType()
+                    != AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE) {
+                EmbeddingIndexingConfig embeddingIndexingConfig =
+                        EmbeddingIndexingConfig.newBuilder()
+                                .setEmbeddingIndexingType(
+                                        convertEmbeddingIndexingTypeToProto(
+                                                embeddingProperty.getIndexingType()))
+                                .build();
+                builder.setEmbeddingIndexingConfig(embeddingIndexingConfig);
+            }
         }
         return builder.build();
     }
@@ -151,6 +171,7 @@
     public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
         Objects.requireNonNull(proto);
         AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
+        builder.setDescription(proto.getDescription());
         List<PropertyConfigProto> properties = proto.getPropertiesList();
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
@@ -174,20 +195,26 @@
                 return toLongPropertyConfig(proto);
             case DOUBLE:
                 return new AppSearchSchema.DoublePropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .build();
             case BOOLEAN:
                 return new AppSearchSchema.BooleanPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .build();
             case BYTES:
                 return new AppSearchSchema.BytesPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .build();
             case DOCUMENT:
                 return toDocumentPropertyConfig(proto);
+            case VECTOR:
+                return toEmbeddingPropertyConfig(proto);
             default:
-                throw new IllegalArgumentException("Invalid dataType: " + proto.getDataType());
+                throw new IllegalArgumentException(
+                        "Invalid dataType code: " + proto.getDataType().getNumber());
         }
     }
 
@@ -196,11 +223,11 @@
             @NonNull PropertyConfigProto proto) {
         AppSearchSchema.StringPropertyConfig.Builder builder =
                 new AppSearchSchema.StringPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setJoinableValueType(
                                 convertJoinableValueTypeFromProto(
                                         proto.getJoinableConfig().getValueType()))
-                        .setDeletionPropagation(proto.getJoinableConfig().getPropagateDelete())
                         .setTokenizerType(
                                 proto.getStringIndexingConfig().getTokenizerType().getNumber());
 
@@ -217,6 +244,7 @@
         AppSearchSchema.DocumentPropertyConfig.Builder builder =
                 new AppSearchSchema.DocumentPropertyConfig.Builder(
                                 proto.getPropertyName(), proto.getSchemaType())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setShouldIndexNestedProperties(
                                 proto.getDocumentIndexingConfig().getIndexNestedProperties());
@@ -230,6 +258,7 @@
             @NonNull PropertyConfigProto proto) {
         AppSearchSchema.LongPropertyConfig.Builder builder =
                 new AppSearchSchema.LongPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber());
 
         // Set indexingType
@@ -241,6 +270,21 @@
     }
 
     @NonNull
+    private static AppSearchSchema.EmbeddingPropertyConfig toEmbeddingPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        AppSearchSchema.EmbeddingPropertyConfig.Builder builder =
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder(proto.getPropertyName())
+                        .setCardinality(proto.getCardinality().getNumber());
+
+        // Set indexingType
+        EmbeddingIndexingConfig.EmbeddingIndexingType.Code embeddingIndexingType =
+                proto.getEmbeddingIndexingConfig().getEmbeddingIndexingType();
+        builder.setIndexingType(convertEmbeddingIndexingTypeFromProto(embeddingIndexingType));
+
+        return builder.build();
+    }
+
+    @NonNull
     private static JoinableConfig.ValueType.Code convertJoinableValueTypeToProto(
             @AppSearchSchema.StringPropertyConfig.JoinableValueType int joinableValueType) {
         switch (joinableValueType) {
@@ -262,12 +306,11 @@
                 return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
             case QUALIFIED_ID:
                 return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
-            default:
-                // Avoid crashing in the 'read' path; we should try to interpret the document to the
-                // extent possible.
-                Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
-                return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
         }
+        // Avoid crashing in the 'read' path; we should try to interpret the document to the
+        // extent possible.
+        Log.w(TAG, "Invalid joinableValueType: " + joinableValueType.getNumber());
+        return AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
     }
 
     @NonNull
@@ -294,12 +337,11 @@
                 return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS;
             case PREFIX:
                 return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES;
-            default:
-                // Avoid crashing in the 'read' path; we should try to interpret the document to the
-                // extent possible.
-                Log.w(TAG, "Invalid indexingType: " + termMatchType.getNumber());
-                return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
         }
+        // Avoid crashing in the 'read' path; we should try to interpret the document to the
+        // extent possible.
+        Log.w(TAG, "Invalid indexingType: " + termMatchType.getNumber());
+        return AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE;
     }
 
     @NonNull
@@ -334,11 +376,39 @@
                 return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
             case RANGE:
                 return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE;
-            default:
-                // Avoid crashing in the 'read' path; we should try to interpret the document to the
-                // extent possible.
-                Log.w(TAG, "Invalid indexingType: " + numericMatchType.getNumber());
-                return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
         }
+        // Avoid crashing in the 'read' path; we should try to interpret the document to the
+        // extent possible.
+        Log.w(TAG, "Invalid indexingType: " + numericMatchType.getNumber());
+        return AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE;
+    }
+
+    @NonNull
+    private static EmbeddingIndexingConfig.EmbeddingIndexingType.Code
+            convertEmbeddingIndexingTypeToProto(
+                    @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType) {
+        switch (indexingType) {
+            case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE:
+                return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.UNKNOWN;
+            case AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY:
+                return EmbeddingIndexingConfig.EmbeddingIndexingType.Code.LINEAR_SEARCH;
+            default:
+                throw new IllegalArgumentException("Invalid indexingType: " + indexingType);
+        }
+    }
+
+    @AppSearchSchema.EmbeddingPropertyConfig.IndexingType
+    private static int convertEmbeddingIndexingTypeFromProto(
+            @NonNull EmbeddingIndexingConfig.EmbeddingIndexingType.Code indexingType) {
+        switch (indexingType) {
+            case UNKNOWN:
+                return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+            case LINEAR_SEARCH:
+                return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY;
+        }
+        // Avoid crashing in the 'read' path; we should try to interpret the document to the
+        // extent possible.
+        Log.w(TAG, "Invalid indexingType: " + indexingType.getNumber());
+        return AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
     }
 }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java
index f4ecb35..53bf2c3 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverter.java
@@ -28,6 +28,7 @@
 import android.app.appsearch.exceptions.AppSearchException;
 
 import com.android.server.appsearch.external.localstorage.AppSearchConfig;
+import com.android.server.appsearch.external.localstorage.SchemaCache;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -38,7 +39,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 
 /**
  * Translates a {@link SearchResultProto} into {@link SearchResult}s.
@@ -52,19 +52,19 @@
      * Translate a {@link SearchResultProto} into {@link SearchResultPage}.
      *
      * @param proto The {@link SearchResultProto} containing results.
-     * @param schemaMap The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>> stores all
-     *     existing prefixed schema type.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @return {@link SearchResultPage} of results.
      */
     @NonNull
     public static SearchResultPage toSearchResultPage(
             @NonNull SearchResultProto proto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull AppSearchConfig config)
             throws AppSearchException {
         List<SearchResult> results = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap, config);
+            SearchResult result =
+                    toUnprefixedSearchResult(proto.getResults(i), schemaCache, config);
             results.add(result);
         }
         return new SearchResultPage(proto.getNextPageToken(), results);
@@ -75,21 +75,20 @@
      * database prefix will be removed from {@link GenericDocument}.
      *
      * @param proto The proto to be converted.
-     * @param schemaMap The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>> stores all
-     *     existing prefixed schema type.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @return A {@link SearchResult}.
      */
     @NonNull
     private static SearchResult toUnprefixedSearchResult(
             @NonNull SearchResultProto.ResultProto proto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull AppSearchConfig config)
             throws AppSearchException {
 
         DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
         String prefix = removePrefixesFromDocument(documentBuilder);
         Map<String, SchemaTypeConfigProto> schemaTypeMap =
-                Objects.requireNonNull(schemaMap.get(prefix));
+                schemaCache.getSchemaMapForPrefix(prefix);
         GenericDocument document =
                 GenericDocumentToProtoConverter.toGenericDocument(
                         documentBuilder, prefix, schemaTypeMap, config);
@@ -97,6 +96,9 @@
                 new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
                         .setGenericDocument(document)
                         .setRankingSignal(proto.getScore());
+        for (int i = 0; i < proto.getAdditionalScoresCount(); i++) {
+            builder.addInformationalRankingSignal(proto.getAdditionalScores(i));
+        }
         if (proto.hasSnippet()) {
             for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
                 SnippetProto.EntryProto entry = proto.getSnippet().getEntries(i);
@@ -116,7 +118,8 @@
                         "Nesting joined results within joined results not allowed.");
             }
 
-            builder.addJoinedResult(toUnprefixedSearchResult(joinedResultProto, schemaMap, config));
+            builder.addJoinedResult(
+                    toUnprefixedSearchResult(joinedResultProto, schemaCache, config));
         }
         return builder.build();
     }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java
index 46e9a7e..5f91f21 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverter.java
@@ -23,6 +23,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.appsearch.EmbeddingVector;
 import android.app.appsearch.FeatureConstants;
 import android.app.appsearch.JoinSpec;
 import android.app.appsearch.SearchResult;
@@ -33,6 +34,7 @@
 import android.util.Log;
 
 import com.android.server.appsearch.external.localstorage.IcingOptionsConfig;
+import com.android.server.appsearch.external.localstorage.SchemaCache;
 import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
 import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
@@ -64,18 +66,22 @@
     private static final String TAG = "AppSearchSearchSpecConv";
     private final String mQueryExpression;
     private final SearchSpec mSearchSpec;
+
     /** The union of allowed prefixes for the top-level SearchSpec and any nested SearchSpecs. */
     private final Set<String> mAllAllowedPrefixes;
+
     /**
      * The intersection of mAllAllowedPrefixes and prefixes requested in the SearchSpec currently
      * being handled.
      */
     private final Set<String> mCurrentSearchSpecPrefixFilters;
+
     /**
      * The intersected prefixed namespaces that are existing in AppSearch and also accessible to the
      * client.
      */
     private final Set<String> mTargetPrefixedNamespaceFilters;
+
     /**
      * The intersected prefixed schema types that are existing in AppSearch and also accessible to
      * the client.
@@ -88,12 +94,8 @@
      */
     private final Map<String, Set<String>> mNamespaceMap;
 
-    /**
-     * The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>} stores all prefixed
-     * schema filters which are stored inAppSearch. This is a field so that we can generated nested
-     * protos.
-     */
-    private final Map<String, Map<String, SchemaTypeConfigProto>> mSchemaMap;
+    /** The SchemaCache instance held in AppSearch. */
+    private final SchemaCache mSchemaCache;
 
     /** Optional config flags in {@link SearchSpecProto}. */
     private final IcingOptionsConfig mIcingOptionsConfig;
@@ -114,21 +116,20 @@
      *     allowed, so nothing will be searched.
      * @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores all
      *     prefixed namespace filters which are stored in AppSearch.
-     * @param schemaMap The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
-     *     stores all prefixed schema filters which are stored inAppSearch.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      */
     public SearchSpecToProtoConverter(
             @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec,
             @NonNull Set<String> allAllowedPrefixes,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull IcingOptionsConfig icingOptionsConfig) {
         mQueryExpression = Objects.requireNonNull(queryExpression);
         mSearchSpec = Objects.requireNonNull(searchSpec);
         mAllAllowedPrefixes = Objects.requireNonNull(allAllowedPrefixes);
         mNamespaceMap = Objects.requireNonNull(namespaceMap);
-        mSchemaMap = Objects.requireNonNull(schemaMap);
+        mSchemaCache = Objects.requireNonNull(schemaCache);
         mIcingOptionsConfig = Objects.requireNonNull(icingOptionsConfig);
 
         // This field holds the prefix filters for the SearchSpec currently being handled, which
@@ -165,7 +166,7 @@
             mTargetPrefixedSchemaFilters =
                     SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
                             mCurrentSearchSpecPrefixFilters,
-                            schemaMap,
+                            schemaCache,
                             searchSpec.getFilterSchemas());
         } else {
             mTargetPrefixedSchemaFilters = new ArraySet<>();
@@ -182,16 +183,17 @@
                         joinSpec.getNestedSearchSpec(),
                         mAllAllowedPrefixes,
                         namespaceMap,
-                        schemaMap,
+                        schemaCache,
                         mIcingOptionsConfig);
     }
 
     /**
-     * @return whether this search's target filters are empty. If any target filter is empty, we
-     *     should skip send request to Icing.
-     *     <p>The nestedConverter is not checked as {@link SearchResult}s from the nested query have
-     *     to be joined to a {@link SearchResult} from the parent query. If the parent query has
-     *     nothing to search, then so does the child query.
+     * Returns whether this search's target filters are empty. If any target filter is empty, we
+     * should skip send request to Icing.
+     *
+     * <p>The nestedConverter is not checked as {@link SearchResult}s from the nested query have to
+     * be joined to a {@link SearchResult} from the parent query. If the parent query has nothing to
+     * search, then so does the child query.
      */
     public boolean hasNothingToSearch() {
         return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
@@ -214,8 +216,8 @@
         removeInaccessibleSchemaFilterCached(
                 callerAccess,
                 visibilityStore,
-                /*inaccessibleSchemaPrefixes=*/ new ArraySet<>(),
-                /*accessibleSchemaPrefixes=*/ new ArraySet<>(),
+                /* inaccessibleSchemaPrefixes= */ new ArraySet<>(),
+                /* accessibleSchemaPrefixes= */ new ArraySet<>(),
                 visibilityChecker);
     }
 
@@ -287,6 +289,13 @@
                         .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters)
                         .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch());
 
+        List<EmbeddingVector> searchEmbeddings = mSearchSpec.getSearchEmbeddings();
+        for (int i = 0; i < searchEmbeddings.size(); i++) {
+            protoBuilder.addEmbeddingQueryVectors(
+                    GenericDocumentToProtoConverter.embeddingVectorToVectorProto(
+                            searchEmbeddings.get(i)));
+        }
+
         // Convert type property filter map into type property mask proto.
         for (Map.Entry<String, List<String>> entry : mSearchSpec.getFilterProperties().entrySet()) {
             if (entry.getKey().equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) {
@@ -316,11 +325,23 @@
         }
         protoBuilder.setTermMatchType(termMatchCodeProto);
 
+        @SearchSpec.EmbeddingSearchMetricType
+        int embeddingSearchMetricType = mSearchSpec.getDefaultEmbeddingSearchMetricType();
+        SearchSpecProto.EmbeddingQueryMetricType.Code embeddingSearchMetricTypeProto =
+                SearchSpecProto.EmbeddingQueryMetricType.Code.forNumber(embeddingSearchMetricType);
+        if (embeddingSearchMetricTypeProto == null
+                || embeddingSearchMetricTypeProto.equals(
+                        SearchSpecProto.EmbeddingQueryMetricType.Code.UNKNOWN)) {
+            throw new IllegalArgumentException(
+                    "Invalid embedding search metric type: " + embeddingSearchMetricType);
+        }
+        protoBuilder.setEmbeddingQueryMetricType(embeddingSearchMetricTypeProto);
+
         if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) {
             JoinSpecProto.NestedSpecProto nestedSpec =
                     JoinSpecProto.NestedSpecProto.newBuilder()
                             .setResultSpec(
-                                    mNestedConverter.toResultSpecProto(mNamespaceMap, mSchemaMap))
+                                    mNestedConverter.toResultSpecProto(mNamespaceMap, mSchemaCache))
                             .setScoringSpec(mNestedConverter.toScoringSpecProto())
                             .setSearchSpec(mNestedConverter.toSearchSpecProto())
                             .build();
@@ -349,18 +370,6 @@
                             + "associated metadata has not yet been turned on.");
         }
 
-        // TODO(b/208654892) Remove this field once EXPERIMENTAL_ICING_ADVANCED_QUERY is fully
-        //  supported.
-        boolean turnOnIcingAdvancedQuery =
-                mSearchSpec.isNumericSearchEnabled()
-                        || mSearchSpec.isVerbatimSearchEnabled()
-                        || mSearchSpec.isListFilterQueryLanguageEnabled()
-                        || mSearchSpec.isListFilterHasPropertyFunctionEnabled();
-        if (turnOnIcingAdvancedQuery) {
-            protoBuilder.setSearchType(
-                    SearchSpecProto.SearchType.Code.EXPERIMENTAL_ICING_ADVANCED_QUERY);
-        }
-
         // Set enabled features
         protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures(mSearchSpec.getEnabledFeatures()));
 
@@ -399,13 +408,11 @@
      *
      * @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores all
      *     existing prefixed namespace.
-     * @param schemaMap The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
-     *     stores all prefixed schema filters which are stored inAppSearch.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      */
     @NonNull
     public ResultSpecProto toResultSpecProto(
-            @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull Map<String, Set<String>> namespaceMap, @NonNull SchemaCache schemaCache) {
         ResultSpecProto.Builder resultSpecBuilder =
                 ResultSpecProto.newBuilder()
                         .setNumPerPage(mSearchSpec.getResultCountPerPage())
@@ -448,7 +455,7 @@
                 addPerSchemaResultGrouping(
                         mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
-                        schemaMap,
+                        schemaCache,
                         resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
                 break;
@@ -464,7 +471,7 @@
                 addPerPackagePerSchemaResultGroupings(
                         mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
-                        schemaMap,
+                        schemaCache,
                         resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE;
                 break;
@@ -473,7 +480,7 @@
                         mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
                         namespaceMap,
-                        schemaMap,
+                        schemaCache,
                         resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
                 break;
@@ -484,7 +491,7 @@
                         mCurrentSearchSpecPrefixFilters,
                         mSearchSpec.getResultGroupingLimit(),
                         namespaceMap,
-                        schemaMap,
+                        schemaCache,
                         resultSpecBuilder);
                 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE;
                 break;
@@ -537,6 +544,8 @@
         addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder);
 
         protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression());
+        protoBuilder.addAllAdditionalAdvancedScoringExpressions(
+                mSearchSpec.getInformationalRankingExpressions());
 
         return protoBuilder.build();
     }
@@ -646,7 +655,7 @@
             String packageName = getPackageName(prefix);
             // Create a new prefix without the database name. This will allow us to group namespaces
             // that have the same name and package but a different database name together.
-            String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/ "");
+            String emptyDatabasePrefix = createPrefix(packageName, /* databaseName= */ "");
             for (String prefixedNamespace : prefixedNamespaces) {
                 String namespace;
                 try {
@@ -676,17 +685,14 @@
      * still be grouped together.
      *
      * @param prefixes Prefixes that we should prepend to all our filters.
-     * @param schemaMap The schema map contains all prefixed existing schema types.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      */
     private static Map<String, List<String>> getSchemaToPrefixedSchemas(
-            @NonNull Set<String> prefixes,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull Set<String> prefixes, @NonNull SchemaCache schemaCache) {
         Map<String, List<String>> schemaToPrefixedSchemas = new ArrayMap<>();
         for (String prefix : prefixes) {
-            Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
-            if (prefixedSchemas == null) {
-                continue;
-            }
+            Map<String, SchemaTypeConfigProto> prefixedSchemas =
+                    schemaCache.getSchemaMapForPrefix(prefix);
             for (String prefixedSchema : prefixedSchemas.keySet()) {
                 String schema;
                 try {
@@ -713,17 +719,14 @@
      * should be grouped together.
      *
      * @param prefixes Prefixes that we should prepend to all our filters.
-     * @param schemaMap The schema map contains all prefixed existing schema types.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      */
     private static Map<String, List<String>> getPackageAndSchemaToPrefixedSchemas(
-            @NonNull Set<String> prefixes,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull Set<String> prefixes, @NonNull SchemaCache schemaCache) {
         Map<String, List<String>> packageAndSchemaToSchemas = new ArrayMap<>();
         for (String prefix : prefixes) {
-            Map<String, SchemaTypeConfigProto> prefixedSchemas = schemaMap.get(prefix);
-            if (prefixedSchemas == null) {
-                continue;
-            }
+            Map<String, SchemaTypeConfigProto> prefixedSchemas =
+                    schemaCache.getSchemaMapForPrefix(prefix);
             String packageName = getPackageName(prefix);
             // Create a new prefix without the database name. This will allow us to group schemas
             // that have the same name and package but a different database name together.
@@ -787,16 +790,16 @@
      *
      * @param prefixes Prefixes that we should prepend to all our filters.
      * @param maxNumResults The maximum number of results for each grouping to support.
-     * @param schemaMap The schema map contains all prefixed existing schema types.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpecs as a specified by client.
      */
     private static void addPerPackagePerSchemaResultGroupings(
             @NonNull Set<String> prefixes,
             int maxNumResults,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> packageAndSchemaToSchemas =
-                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
             List<ResultSpecProto.ResultGrouping.Entry> entries =
@@ -820,19 +823,19 @@
      * @param prefixes Prefixes that we should prepend to all our filters.
      * @param maxNumResults The maximum number of results for each grouping to support.
      * @param namespaceMap The namespace map contains all prefixed existing namespaces.
-     * @param schemaMap The schema map contains all prefixed existing schema types.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpec as specified by client.
      */
     private static void addPerPackagePerNamespacePerSchemaResultGrouping(
             @NonNull Set<String> prefixes,
             int maxNumResults,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> packageAndNamespaceToNamespaces =
                 getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
         Map<String, List<String>> packageAndSchemaToSchemas =
-                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) {
             for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) {
@@ -945,16 +948,16 @@
      *
      * @param prefixes Prefixes that we should prepend to all our filters.
      * @param maxNumResults The maximum number of results for each grouping to support.
-     * @param schemaMap The schema map contains all prefixed existing schema types.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpec as specified by client.
      */
     private static void addPerSchemaResultGrouping(
             @NonNull Set<String> prefixes,
             int maxNumResults,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> schemaToPrefixedSchemas =
-                getSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
             List<ResultSpecProto.ResultGrouping.Entry> entries =
@@ -978,19 +981,19 @@
      * @param prefixes Prefixes that we should prepend to all our filters.
      * @param maxNumResults The maximum number of results for each grouping to support.
      * @param namespaceMap The namespace map contains all prefixed existing namespaces.
-     * @param schemaMap The schema map contains all prefixed existing schema types.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @param resultSpecBuilder ResultSpec as specified by client.
      */
     private static void addPerNamespaceAndSchemaResultGrouping(
             @NonNull Set<String> prefixes,
             int maxNumResults,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull ResultSpecProto.Builder resultSpecBuilder) {
         Map<String, List<String>> namespaceToPrefixedNamespaces =
                 getNamespaceToPrefixedNamespaces(prefixes, namespaceMap);
         Map<String, List<String>> schemaToPrefixedSchemas =
-                getSchemaToPrefixedSchemas(prefixes, schemaMap);
+                getSchemaToPrefixedSchemas(prefixes, schemaCache);
 
         for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) {
             for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) {
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterUtil.java b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterUtil.java
index 904511a..80af25d 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterUtil.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterUtil.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.util.ArraySet;
 
+import com.android.server.appsearch.external.localstorage.SchemaCache;
+
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 
 import java.util.List;
@@ -75,32 +77,28 @@
      * intersection set with those prefixed schema candidates that are stored in AppSearch.
      *
      * @param prefixes Set of database prefix which the caller want to access.
-     * @param schemaMap The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
-     *     stores all prefixed schema filters which are stored in AppSearch.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
      * @param inputSchemaFilters The set contains all desired but un-prefixed namespace filters of
      *     user. If the inputSchemaFilters is empty, all existing prefixedCandidates will be added
      *     to the prefixedTargetFilters.
      */
     static Set<String> generateTargetSchemaFilters(
             @NonNull Set<String> prefixes,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap,
+            @NonNull SchemaCache schemaCache,
             @NonNull List<String> inputSchemaFilters) {
         Set<String> targetPrefixedSchemaFilters = new ArraySet<>();
         // Append prefix to input schema filters and get the intersection of existing schema filter.
         for (String prefix : prefixes) {
             // Step1: find all prefixed schema candidates that are stored in AppSearch.
-            Map<String, SchemaTypeConfigProto> prefixedSchemaMap = schemaMap.get(prefix);
-            if (prefixedSchemaMap == null) {
-                // This is should never happen. All prefixes should be verified before reach
-                // here.
-                continue;
-            }
+            Map<String, SchemaTypeConfigProto> prefixedSchemaMap =
+                    schemaCache.getSchemaMapForPrefix(prefix);
             Set<String> prefixedSchemaCandidates = prefixedSchemaMap.keySet();
-            // Step2: get the intersection of user searching filters and those candidates which are
-            // stored in AppSearch.
-            addIntersectedFilters(
+            // Step2: get the intersection of user searching filters (after polymorphism
+            // expansion) and those candidates which are stored in AppSearch.
+            addIntersectedPolymorphicSchemaFilters(
                     prefix,
                     prefixedSchemaCandidates,
+                    schemaCache,
                     inputSchemaFilters,
                     targetPrefixedSchemaFilters);
         }
@@ -137,4 +135,44 @@
             }
         }
     }
+
+    /**
+     * Find the schema intersection set of candidates existing in AppSearch and user specified
+     * schema filters after polymorphism expansion.
+     *
+     * @param prefix The package and database's identifier.
+     * @param prefixedCandidates The set contains all prefixed candidates which are existing in a
+     *     database.
+     * @param schemaCache The SchemaCache instance held in AppSearch.
+     * @param inputFilters The set contains all desired but un-prefixed filters of user. If the
+     *     inputFilters is empty, all prefixedCandidates will be added to the prefixedTargetFilters.
+     * @param prefixedTargetFilters The output set contains all desired prefixed filters which are
+     *     existing in the database.
+     */
+    private static void addIntersectedPolymorphicSchemaFilters(
+            @NonNull String prefix,
+            @NonNull Set<String> prefixedCandidates,
+            @NonNull SchemaCache schemaCache,
+            @NonNull List<String> inputFilters,
+            @NonNull Set<String> prefixedTargetFilters) {
+        if (inputFilters.isEmpty()) {
+            // Client didn't specify certain schemas to search over, add all candidates.
+            // Polymorphism expansion is not necessary here, since expanding the set of all
+            // schema types will result in the same set of schema types.
+            prefixedTargetFilters.addAll(prefixedCandidates);
+            return;
+        }
+
+        Set<String> currentPrefixedTargetFilters = new ArraySet<>();
+        for (int i = 0; i < inputFilters.size(); i++) {
+            String prefixedTargetSchemaFilter = prefix + inputFilters.get(i);
+            if (prefixedCandidates.contains(prefixedTargetSchemaFilter)) {
+                currentPrefixedTargetFilters.add(prefixedTargetSchemaFilter);
+            }
+        }
+        // Expand schema filters by polymorphism.
+        currentPrefixedTargetFilters =
+                schemaCache.getSchemaTypesWithDescendants(prefix, currentPrefixedTargetFilters);
+        prefixedTargetFilters.addAll(currentPrefixedTargetFilters);
+    }
 }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverter.java b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
index 1639188..d54c37c 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
@@ -19,8 +19,9 @@
 import android.annotation.NonNull;
 import android.app.appsearch.SearchSuggestionSpec;
 
+import com.android.server.appsearch.external.localstorage.SchemaCache;
+
 import com.google.android.icing.proto.NamespaceDocumentUriGroup;
-import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SuggestionScoringSpecProto;
 import com.google.android.icing.proto.SuggestionSpecProto;
 import com.google.android.icing.proto.TermMatchType;
@@ -39,16 +40,19 @@
 public final class SearchSuggestionSpecToProtoConverter {
     private final String mSuggestionQueryExpression;
     private final SearchSuggestionSpec mSearchSuggestionSpec;
+
     /**
      * The client specific packages and databases to search for. For local storage, this always
      * contains a single prefix.
      */
     private final Set<String> mPrefixes;
+
     /**
      * The intersected prefixed namespaces that are existing in AppSearch and also accessible to the
      * client.
      */
     private final Set<String> mTargetPrefixedNamespaceFilters;
+
     /**
      * The intersected prefixed schema types that are existing in AppSearch and also accessible to
      * the client.
@@ -70,7 +74,7 @@
             @NonNull SearchSuggestionSpec searchSuggestionSpec,
             @NonNull Set<String> prefixes,
             @NonNull Map<String, Set<String>> namespaceMap,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+            @NonNull SchemaCache schemaCache) {
         mSuggestionQueryExpression = Objects.requireNonNull(suggestionQueryExpression);
         mSearchSuggestionSpec = Objects.requireNonNull(searchSuggestionSpec);
         mPrefixes = Objects.requireNonNull(prefixes);
@@ -80,12 +84,12 @@
                         prefixes, namespaceMap, searchSuggestionSpec.getFilterNamespaces());
         mTargetPrefixedSchemaFilters =
                 SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
-                        prefixes, schemaMap, searchSuggestionSpec.getFilterSchemas());
+                        prefixes, schemaCache, searchSuggestionSpec.getFilterSchemas());
     }
 
     /**
-     * @return whether this search's target filters are empty. If any target filter is empty, we
-     *     should skip send request to Icing.
+     * Returns whether this search's target filters are empty. If any target filter is empty, we
+     * should skip send request to Icing.
      */
     public boolean hasNothingToSearch() {
         return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/CallStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/CallStats.java
index 5f59000..30a482c 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/stats/CallStats.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/CallStats.java
@@ -44,6 +44,7 @@
  * @hide
  */
 public class CallStats {
+    /** Call types. */
     @IntDef(
             value = {
                 CALL_TYPE_UNKNOWN,
@@ -77,6 +78,7 @@
                 CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
                 CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
                 CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
+                CALL_TYPE_EXECUTE_APP_FUNCTION
             })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CallType {}
@@ -112,6 +114,7 @@
     public static final int CALL_TYPE_REGISTER_OBSERVER_CALLBACK = 28;
     public static final int CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK = 29;
     public static final int CALL_TYPE_GLOBAL_GET_NEXT_PAGE = 30;
+    public static final int CALL_TYPE_EXECUTE_APP_FUNCTION = 31;
 
     // These strings are for the subset of call types that correspond to an AppSearchManager API
     private static final String CALL_TYPE_STRING_INITIALIZE = "initialize";
@@ -143,9 +146,11 @@
     private static final String CALL_TYPE_STRING_UNREGISTER_OBSERVER_CALLBACK =
             "globalUnregisterObserverCallback";
     private static final String CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE = "globalGetNextPage";
+    private static final String CALL_TYPE_STRING_EXECUTE_APP_FUNCTION = "executeAppFunction";
 
     @Nullable private final String mPackageName;
     @Nullable private final String mDatabase;
+
     /**
      * The status code returned by {@link AppSearchResult#getResultCode()} for the call or internal
      * state.
@@ -392,6 +397,8 @@
                 return CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK;
             case CALL_TYPE_STRING_GLOBAL_GET_NEXT_PAGE:
                 return CALL_TYPE_GLOBAL_GET_NEXT_PAGE;
+            case CALL_TYPE_STRING_EXECUTE_APP_FUNCTION:
+                return CALL_TYPE_EXECUTE_APP_FUNCTION;
             default:
                 return CALL_TYPE_UNKNOWN;
         }
@@ -426,6 +433,7 @@
                         CALL_TYPE_GET_STORAGE_INFO,
                         CALL_TYPE_REGISTER_OBSERVER_CALLBACK,
                         CALL_TYPE_UNREGISTER_OBSERVER_CALLBACK,
-                        CALL_TYPE_GLOBAL_GET_NEXT_PAGE));
+                        CALL_TYPE_GLOBAL_GET_NEXT_PAGE,
+                        CALL_TYPE_EXECUTE_APP_FUNCTION));
     }
 }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/ClickStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/ClickStats.java
new file mode 100644
index 0000000..e74a4e4
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/ClickStats.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.stats;
+
+import android.annotation.NonNull;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+import java.util.Objects;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a click action, converted from {@link
+ * android.app.appsearch.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @hide
+ */
+public class ClickStats {
+    private final long mTimestampMillis;
+
+    private final long mTimeStayOnResultMillis;
+
+    private final int mResultRankInBlock;
+
+    private final int mResultRankGlobal;
+
+    private final boolean mIsGoodClick;
+
+    ClickStats(@NonNull Builder builder) {
+        Objects.requireNonNull(builder);
+        mTimestampMillis = builder.mTimestampMillis;
+        mTimeStayOnResultMillis = builder.mTimeStayOnResultMillis;
+        mResultRankInBlock = builder.mResultRankInBlock;
+        mResultRankGlobal = builder.mResultRankGlobal;
+        mIsGoodClick = builder.mIsGoodClick;
+    }
+
+    /** Returns the click action timestamp in milliseconds since Unix epoch. */
+    public long getTimestampMillis() {
+        return mTimestampMillis;
+    }
+
+    /** Returns the time (duration) of the user staying on the clicked result. */
+    public long getTimeStayOnResultMillis() {
+        return mTimeStayOnResultMillis;
+    }
+
+    /** Returns the in-block rank of the clicked result. */
+    public int getResultRankInBlock() {
+        return mResultRankInBlock;
+    }
+
+    /** Returns the global rank of the clicked result. */
+    public int getResultRankGlobal() {
+        return mResultRankGlobal;
+    }
+
+    /**
+     * Returns whether this click is a good click or not.
+     *
+     * @see Builder#setIsGoodClick
+     */
+    public boolean isGoodClick() {
+        return mIsGoodClick;
+    }
+
+    /** Builder for {@link ClickStats} */
+    public static final class Builder {
+        private long mTimestampMillis;
+
+        private long mTimeStayOnResultMillis;
+
+        private int mResultRankInBlock;
+
+        private int mResultRankGlobal;
+
+        private boolean mIsGoodClick = true;
+
+        /** Sets the click action timestamp in milliseconds since Unix epoch. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimestampMillis(long timestampMillis) {
+            mTimestampMillis = timestampMillis;
+            return this;
+        }
+
+        /** Sets the time (duration) of the user staying on the clicked result. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+            mTimeStayOnResultMillis = timeStayOnResultMillis;
+            return this;
+        }
+
+        /** Sets the in-block rank of the clicked result. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankInBlock(int resultRankInBlock) {
+            mResultRankInBlock = resultRankInBlock;
+            return this;
+        }
+
+        /** Sets the global rank of the clicked result. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankGlobal(int resultRankGlobal) {
+            mResultRankGlobal = resultRankGlobal;
+            return this;
+        }
+
+        /**
+         * Sets the flag indicating whether the click is good or not.
+         *
+         * <p>A good click means the user is satisfied by the clicked document. The caller should
+         * define its own criteria and set this field accordingly.
+         *
+         * <p>The default value is true if unset. We should treat it as a good click by default if
+         * the caller didn't specify or could not determine for several reasons:
+         *
+         * <ul>
+         *   <li>It may be difficult for the caller to determine if the user is satisfied by the
+         *       clicked document or not.
+         *   <li>AppSearch collects search quality metrics that are related to number of good
+         *       clicks. We don't want to demote the quality score aggressively by the undetermined
+         *       ones.
+         * </ul>
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setIsGoodClick(boolean isGoodClick) {
+            mIsGoodClick = isGoodClick;
+            return this;
+        }
+
+        /** Builds a new {@link ClickStats} from the {@link ClickStats.Builder}. */
+        @NonNull
+        public ClickStats build() {
+            return new ClickStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/InitializeStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/InitializeStats.java
index d2da318..2a75c91 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/stats/InitializeStats.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/InitializeStats.java
@@ -80,35 +80,47 @@
 
     @AppSearchResult.ResultCode private final int mStatusCode;
     private final int mTotalLatencyMillis;
+
     /** Whether the initialize() detects deSync. */
     private final boolean mHasDeSync;
+
     /** Time used to read and process the schema and namespaces. */
     private final int mPrepareSchemaAndNamespacesLatencyMillis;
+
     /** Time used to read and process the visibility store. */
     private final int mPrepareVisibilityStoreLatencyMillis;
+
     /** Overall time used for the native function call. */
     private final int mNativeLatencyMillis;
 
     @RecoveryCause private final int mNativeDocumentStoreRecoveryCause;
     @RecoveryCause private final int mNativeIndexRestorationCause;
     @RecoveryCause private final int mNativeSchemaStoreRecoveryCause;
+
     /** Time used to recover the document store. */
     private final int mNativeDocumentStoreRecoveryLatencyMillis;
+
     /** Time used to restore the index. */
     private final int mNativeIndexRestorationLatencyMillis;
+
     /** Time used to recover the schema store. */
     private final int mNativeSchemaStoreRecoveryLatencyMillis;
+
     /** Status regarding how much data is lost during the initialization. */
     private final int mNativeDocumentStoreDataStatus;
+
     /**
      * Returns number of documents currently in document store. Those may include alive, deleted,
      * and expired documents.
      */
     private final int mNativeNumDocuments;
+
     /** Returns number of schema types currently in the schema store. */
     private final int mNativeNumSchemaTypes;
+
     /** Whether we had to reset the index, losing all data, during initialization. */
     private final boolean mHasReset;
+
     /** If we had to reset, contains the status code of the reset operation. */
     @AppSearchResult.ResultCode private final int mResetStatusCode;
 
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/PutDocumentStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/PutDocumentStats.java
index 8d0866f..2e9c508 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/stats/PutDocumentStats.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/PutDocumentStats.java
@@ -31,6 +31,7 @@
 public final class PutDocumentStats {
     @NonNull private final String mPackageName;
     @NonNull private final String mDatabase;
+
     /**
      * The status code returned by {@link AppSearchResult#getResultCode()} for the call or internal
      * state.
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/RemoveStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/RemoveStats.java
index ef8c736..8dcfa4e 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/stats/RemoveStats.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/RemoveStats.java
@@ -35,6 +35,7 @@
  * @hide
  */
 public final class RemoveStats {
+    /** Types of stats available for remove API. */
     @IntDef(
             value = {
                 // It needs to be sync with DeleteType.Code in
@@ -50,17 +51,22 @@
 
     /** Default. Should never be used. */
     public static final int UNKNOWN = 0;
+
     /** Delete by namespace + id. */
     public static final int SINGLE = 1;
+
     /** Delete by query. */
     public static final int QUERY = 2;
+
     /** Delete by namespace. */
     public static final int NAMESPACE = 3;
+
     /** Delete by schema type. */
     public static final int SCHEMA_TYPE = 4;
 
     @NonNull private final String mPackageName;
     @NonNull private final String mDatabase;
+
     /**
      * The status code returned by {@link AppSearchResult#getResultCode()} for the call or internal
      * state.
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/SearchIntentStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/SearchIntentStats.java
new file mode 100644
index 0000000..62ad199
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/SearchIntentStats.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.stats;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search intent, converted from {@link
+ * android.app.appsearch.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * <p>A search intent includes a valid AppSearch search request, potentially followed by several
+ * user click actions (see {@link ClickStats}) on fetched result documents. Related information of a
+ * search intent will be extracted from {@link
+ * android.app.appsearch.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @hide
+ */
+public final class SearchIntentStats {
+    /** AppSearch query correction type compared with the previous query. */
+    @IntDef(
+            value = {
+                QUERY_CORRECTION_TYPE_UNKNOWN,
+                QUERY_CORRECTION_TYPE_FIRST_QUERY,
+                QUERY_CORRECTION_TYPE_REFINEMENT,
+                QUERY_CORRECTION_TYPE_ABANDONMENT,
+                QUERY_CORRECTION_TYPE_END_SESSION,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface QueryCorrectionType {}
+
+    public static final int QUERY_CORRECTION_TYPE_UNKNOWN = 0;
+
+    public static final int QUERY_CORRECTION_TYPE_FIRST_QUERY = 1;
+
+    public static final int QUERY_CORRECTION_TYPE_REFINEMENT = 2;
+
+    public static final int QUERY_CORRECTION_TYPE_ABANDONMENT = 3;
+
+    public static final int QUERY_CORRECTION_TYPE_END_SESSION = 4;
+
+    @NonNull private final String mPackageName;
+
+    @Nullable private final String mDatabase;
+
+    @Nullable private final String mPrevQuery;
+
+    @Nullable private final String mCurrQuery;
+
+    private final long mTimestampMillis;
+
+    private final int mNumResultsFetched;
+
+    @QueryCorrectionType private final int mQueryCorrectionType;
+
+    @NonNull private final List<ClickStats> mClicksStats;
+
+    SearchIntentStats(@NonNull Builder builder) {
+        Objects.requireNonNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mPrevQuery = builder.mPrevQuery;
+        mCurrQuery = builder.mCurrQuery;
+        mTimestampMillis = builder.mTimestampMillis;
+        mNumResultsFetched = builder.mNumResultsFetched;
+        mQueryCorrectionType = builder.mQueryCorrectionType;
+        mClicksStats = builder.mClicksStats;
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Returns calling database name.
+     *
+     * <p>For global search, database name will be null.
+     */
+    @Nullable
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns the raw query string of the previous search intent. */
+    @Nullable
+    public String getPrevQuery() {
+        return mPrevQuery;
+    }
+
+    /** Returns the raw query string of this (current) search intent. */
+    @Nullable
+    public String getCurrQuery() {
+        return mCurrQuery;
+    }
+
+    /** Returns the search intent timestamp in milliseconds since Unix epoch. */
+    public long getTimestampMillis() {
+        return mTimestampMillis;
+    }
+
+    /**
+     * Returns total number of results fetched from AppSearch by the client in this search intent.
+     */
+    public int getNumResultsFetched() {
+        return mNumResultsFetched;
+    }
+
+    /**
+     * Returns the correction type of the query in this search intent compared with the previous
+     * search intent. Default value: {@link SearchIntentStats#QUERY_CORRECTION_TYPE_UNKNOWN}.
+     */
+    @QueryCorrectionType
+    public int getQueryCorrectionType() {
+        return mQueryCorrectionType;
+    }
+
+    /** Returns the list of {@link ClickStats} in this search intent. */
+    @NonNull
+    public List<ClickStats> getClicksStats() {
+        return mClicksStats;
+    }
+
+    /** Builder for {@link SearchIntentStats} */
+    public static final class Builder {
+        @NonNull private final String mPackageName;
+
+        @Nullable private String mDatabase;
+
+        @Nullable private String mPrevQuery;
+
+        @Nullable private String mCurrQuery;
+
+        private long mTimestampMillis;
+
+        private int mNumResultsFetched;
+
+        @QueryCorrectionType private int mQueryCorrectionType = QUERY_CORRECTION_TYPE_UNKNOWN;
+
+        @NonNull private List<ClickStats> mClicksStats = new ArrayList<>();
+
+        private boolean mBuilt = false;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName) {
+            mPackageName = Objects.requireNonNull(packageName);
+        }
+
+        /** Constructor the {@link Builder} from an existing {@link SearchIntentStats}. */
+        public Builder(@NonNull SearchIntentStats searchIntentStats) {
+            Objects.requireNonNull(searchIntentStats);
+
+            mPackageName = searchIntentStats.getPackageName();
+            mDatabase = searchIntentStats.getDatabase();
+            mPrevQuery = searchIntentStats.getPrevQuery();
+            mCurrQuery = searchIntentStats.getCurrQuery();
+            mTimestampMillis = searchIntentStats.getTimestampMillis();
+            mNumResultsFetched = searchIntentStats.getNumResultsFetched();
+            mQueryCorrectionType = searchIntentStats.getQueryCorrectionType();
+            mClicksStats.addAll(searchIntentStats.getClicksStats());
+        }
+
+        /**
+         * Sets calling database name.
+         *
+         * <p>For global search, database name will be null.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setDatabase(@Nullable String database) {
+            resetIfBuilt();
+            mDatabase = database;
+            return this;
+        }
+
+        /** Sets the raw query string of the previous search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setPrevQuery(@Nullable String prevQuery) {
+            resetIfBuilt();
+            mPrevQuery = prevQuery;
+            return this;
+        }
+
+        /** Sets the raw query string of this (current) search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setCurrQuery(@Nullable String currQuery) {
+            resetIfBuilt();
+            mCurrQuery = currQuery;
+            return this;
+        }
+
+        /** Sets the search intent timestamp in milliseconds since Unix epoch. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimestampMillis(long timestampMillis) {
+            resetIfBuilt();
+            mTimestampMillis = timestampMillis;
+            return this;
+        }
+
+        /**
+         * Sets total number of results fetched from AppSearch by the client in this search intent.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setNumResultsFetched(int numResultsFetched) {
+            resetIfBuilt();
+            mNumResultsFetched = numResultsFetched;
+            return this;
+        }
+
+        /**
+         * Sets the correction type of the query in this search intent compared with the previous
+         * search intent.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQueryCorrectionType(@QueryCorrectionType int queryCorrectionType) {
+            resetIfBuilt();
+            mQueryCorrectionType = queryCorrectionType;
+            return this;
+        }
+
+        /** Adds one or more {@link ClickStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addClicksStats(@NonNull ClickStats... clicksStats) {
+            Objects.requireNonNull(clicksStats);
+            resetIfBuilt();
+            return addClicksStats(Arrays.asList(clicksStats));
+        }
+
+        /** Adds a collection of {@link ClickStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addClicksStats(@NonNull Collection<? extends ClickStats> clicksStats) {
+            Objects.requireNonNull(clicksStats);
+            resetIfBuilt();
+            mClicksStats.addAll(clicksStats);
+            return this;
+        }
+
+        /**
+         * If built, make a copy of previous data for every field so that the builder can be reused.
+         */
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mClicksStats = new ArrayList<>(mClicksStats);
+                mBuilt = false;
+            }
+        }
+
+        /** Builds a new {@link SearchIntentStats} from the {@link Builder}. */
+        @NonNull
+        public SearchIntentStats build() {
+            mBuilt = true;
+            return new SearchIntentStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/SearchSessionStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/SearchSessionStats.java
new file mode 100644
index 0000000..4b5b529
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/SearchSessionStats.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.stats;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search session, converted from {@link
+ * android.app.appsearch.PutDocumentsRequest#getTakenActionGenericDocuments}. It contains a list of
+ * {@link SearchIntentStats} and aggregated metrics of them.
+ *
+ * <p>A search session is consist of a sequence of related search intents. See {@link
+ * SearchIntentStats} for more details.
+ *
+ * @hide
+ */
+public final class SearchSessionStats {
+    @NonNull private final String mPackageName;
+
+    @Nullable private final String mDatabase;
+
+    @NonNull private final List<SearchIntentStats> mSearchIntentsStats;
+
+    SearchSessionStats(@NonNull Builder builder) {
+        Objects.requireNonNull(builder);
+        mPackageName = builder.mPackageName;
+        mDatabase = builder.mDatabase;
+        mSearchIntentsStats = builder.mSearchIntentsStats;
+    }
+
+    /**
+     * Returns a nullable {@link SearchIntentStats} instance containing information of the last
+     * search intent which ended the search session.
+     *
+     * <p>If {@link #getSearchIntentsStats} is empty (i.e. the caller didn't add any {@link
+     * SearchIntentStats} via {@link Builder#addSearchIntentsStats}), then return null.
+     *
+     * <p>It is similar to the last element in {@link #getSearchIntentsStats}, except there is no
+     * previous query and the query correction type is tagged as {@link
+     * SearchIntentStats#QUERY_CORRECTION_TYPE_END_SESSION}.
+     *
+     * <p>This stats is useful to determine whether the user ended the search session with
+     * satisfaction (i.e. had found desired result documents) or not.
+     */
+    @Nullable
+    public SearchIntentStats getEndSessionSearchIntentStats() {
+        if (mSearchIntentsStats.isEmpty()) {
+            return null;
+        }
+
+        SearchIntentStats lastSearchIntentStats =
+                mSearchIntentsStats.get(mSearchIntentsStats.size() - 1);
+        return new SearchIntentStats.Builder(lastSearchIntentStats)
+                .setPrevQuery(null)
+                .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_END_SESSION)
+                .build();
+    }
+
+    /** Returns calling package name. */
+    @NonNull
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Returns calling database name.
+     *
+     * <p>For global search, database name will be null.
+     */
+    @Nullable
+    public String getDatabase() {
+        return mDatabase;
+    }
+
+    /** Returns the list of {@link SearchIntentStats} in this search session. */
+    @NonNull
+    public List<SearchIntentStats> getSearchIntentsStats() {
+        return mSearchIntentsStats;
+    }
+
+    /** Builder for {@link SearchSessionStats}. */
+    public static final class Builder {
+        @NonNull private final String mPackageName;
+
+        @Nullable private String mDatabase;
+
+        @NonNull private List<SearchIntentStats> mSearchIntentsStats = new ArrayList<>();
+
+        private boolean mBuilt = false;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName) {
+            mPackageName = Objects.requireNonNull(packageName);
+        }
+
+        /**
+         * Sets calling database name.
+         *
+         * <p>For global search, database name will be null.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setDatabase(@Nullable String database) {
+            resetIfBuilt();
+            mDatabase = database;
+            return this;
+        }
+
+        /** Adds one or more {@link SearchIntentStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addSearchIntentsStats(@NonNull SearchIntentStats... searchIntentsStats) {
+            Objects.requireNonNull(searchIntentsStats);
+            resetIfBuilt();
+            return addSearchIntentsStats(Arrays.asList(searchIntentsStats));
+        }
+
+        /** Adds a collection of {@link SearchIntentStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder addSearchIntentsStats(
+                @NonNull Collection<? extends SearchIntentStats> searchIntentsStats) {
+            Objects.requireNonNull(searchIntentsStats);
+            resetIfBuilt();
+            mSearchIntentsStats.addAll(searchIntentsStats);
+            return this;
+        }
+
+        /**
+         * If built, make a copy of previous data for every field so that the builder can be reused.
+         */
+        private void resetIfBuilt() {
+            if (mBuilt) {
+                mSearchIntentsStats = new ArrayList<>(mSearchIntentsStats);
+                mBuilt = false;
+            }
+        }
+
+        /** Builds a new {@link SearchSessionStats} from the {@link Builder}. */
+        @NonNull
+        public SearchSessionStats build() {
+            mBuilt = true;
+            return new SearchSessionStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/stats/SearchStats.java b/service/java/com/android/server/appsearch/external/localstorage/stats/SearchStats.java
index d0db41a..d23459b 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/stats/SearchStats.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/stats/SearchStats.java
@@ -36,6 +36,7 @@
  * @hide
  */
 public final class SearchStats {
+    /** Types of Visibility scopes available for search. */
     @IntDef(
             value = {
                 // Searches apps' own documents.
@@ -61,6 +62,7 @@
 
     @NonNull private final String mPackageName;
     @Nullable private final String mDatabase;
+
     /**
      * The status code returned by {@link AppSearchResult#getResultCode()} for the call or internal
      * state.
@@ -68,72 +70,98 @@
     @AppSearchResult.ResultCode private final int mStatusCode;
 
     private final int mTotalLatencyMillis;
+
     /** Time used to rewrite the search spec. */
     private final int mRewriteSearchSpecLatencyMillis;
+
     /** Time used to rewrite the search results. */
     private final int mRewriteSearchResultLatencyMillis;
+
     /** Time passed while waiting to acquire the lock during Java function calls. */
     private final int mJavaLockAcquisitionLatencyMillis;
+
     /**
      * Time spent on ACL checking. This is the time spent filtering namespaces based on package
      * permissions and Android permission access.
      */
     private final int mAclCheckLatencyMillis;
+
     /** Defines the scope the query is searching over. */
     @VisibilityScope private final int mVisibilityScope;
+
     /** Overall time used for the native function call. */
     private final int mNativeLatencyMillis;
+
     /** Number of terms in the query string. */
     private final int mNativeNumTerms;
+
     /** Length of the query string. */
     private final int mNativeQueryLength;
+
     /** Number of namespaces filtered. */
     private final int mNativeNumNamespacesFiltered;
+
     /** Number of schema types filtered. */
     private final int mNativeNumSchemaTypesFiltered;
+
     /** The requested number of results in one page. */
     private final int mNativeRequestedPageSize;
+
     /** The actual number of results returned in the current page. */
     private final int mNativeNumResultsReturnedCurrentPage;
+
     /**
      * Whether the function call is querying the first page. If it's not, Icing will fetch the
      * results from cache so that some steps may be skipped.
      */
     private final boolean mNativeIsFirstPage;
+
     /**
      * Time used to parse the query, including 2 parts: tokenizing and transforming tokens into an
      * iterator tree.
      */
     private final int mNativeParseQueryLatencyMillis;
+
     /** Strategy of scoring and ranking. */
     @SearchSpec.RankingStrategy private final int mNativeRankingStrategy;
+
     /** Number of documents scored. */
     private final int mNativeNumDocumentsScored;
+
     /** Time used to score the raw results. */
     private final int mNativeScoringLatencyMillis;
+
     /** Time used to rank the scored results. */
     private final int mNativeRankingLatencyMillis;
+
     /**
      * Time used to fetch the document protos. Note that it includes the time to snippet if {@link
      * SearchStats#mNativeNumResultsWithSnippets} is greater than 0.
      */
     private final int mNativeDocumentRetrievingLatencyMillis;
+
     /** How many snippets are calculated. */
     private final int mNativeNumResultsWithSnippets;
+
     /** Time passed while waiting to acquire the lock during native function calls. */
     private final int mNativeLockAcquisitionLatencyMillis;
+
     /** Time used to send data across the JNI boundary from java to native side. */
     private final int mJavaToNativeJniLatencyMillis;
+
     /** Time used to send data across the JNI boundary from native to java side. */
     private final int mNativeToJavaJniLatencyMillis;
+
     /** The type of join performed. Zero if no join is performed */
     @JoinableValueType private final int mJoinType;
+
     /** The total number of joined documents in the current page. */
     private final int mNativeNumJoinedResultsCurrentPage;
+
     /** Time taken to join documents together. */
     private final int mNativeJoinLatencyMillis;
 
-    private final String mSearchSourceLogTag;
+    @Nullable private final String mSearchSourceLogTag;
 
     SearchStats(@NonNull Builder builder) {
         Objects.requireNonNull(builder);
@@ -378,7 +406,7 @@
         @JoinableValueType int mJoinType;
         int mNativeNumJoinedResultsCurrentPage;
         int mNativeJoinLatencyMillis;
-        String mSearchSourceLogTag;
+        @Nullable String mSearchSourceLogTag;
 
         /**
          * Constructor
@@ -628,7 +656,7 @@
         /** Sets a tag to indicate the source of this search. */
         @CanIgnoreReturnValue
         @NonNull
-        public Builder setSearchSourceLogTag(@NonNull String searchSourceLogTag) {
+        public Builder setSearchSourceLogTag(@Nullable String searchSourceLogTag) {
             mSearchSourceLogTag = searchSourceLogTag;
             return this;
         }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/usagereporting/ClickActionGenericDocument.java b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/ClickActionGenericDocument.java
new file mode 100644
index 0000000..9e9db31
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/ClickActionGenericDocument.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.usagereporting.ActionConstants;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Wrapper class for
+ *
+ * <p>click action
+ *
+ * <p>{@link GenericDocument}, which contains getters for click action properties.
+ *
+ * @hide
+ */
+public class ClickActionGenericDocument extends TakenActionGenericDocument {
+    private static final String PROPERTY_PATH_QUERY = "query";
+    private static final String PROPERTY_PATH_RESULT_RANK_IN_BLOCK = "resultRankInBlock";
+    private static final String PROPERTY_PATH_RESULT_RANK_GLOBAL = "resultRankGlobal";
+    private static final String PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS = "timeStayOnResultMillis";
+
+    ClickActionGenericDocument(@NonNull GenericDocument document) {
+        super(Objects.requireNonNull(document));
+    }
+
+    /** Returns the string value of property {@code query}. */
+    @Nullable
+    public String getQuery() {
+        return getPropertyString(PROPERTY_PATH_QUERY);
+    }
+
+    /** Returns the integer value of property {@code resultRankInBlock}. */
+    public int getResultRankInBlock() {
+        return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK);
+    }
+
+    /** Returns the integer value of property {@code resultRankGlobal}. */
+    public int getResultRankGlobal() {
+        return (int) getPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL);
+    }
+
+    /** Returns the long value of property {@code timeStayOnResultMillis}. */
+    public long getTimeStayOnResultMillis() {
+        return getPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS);
+    }
+
+    /** Builder for {@link ClickActionGenericDocument}. */
+    public static final class Builder extends TakenActionGenericDocument.Builder<Builder> {
+        /**
+         * Creates a new {@link ClickActionGenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace the namespace to set for the {@link GenericDocument}.
+         * @param id the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *     provided {@code schemaType} must be defined using {@link AppSearchSession#setSchema}
+         *     prior to inserting a document of this {@code schemaType} into the AppSearch index
+         *     using {@link AppSearchSession#put}. Otherwise, the document will be rejected by
+         *     {@link AppSearchSession#put} with result code {@link
+         *     AppSearchResult#RESULT_NOT_FOUND}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            super(
+                    Objects.requireNonNull(namespace),
+                    Objects.requireNonNull(id),
+                    Objects.requireNonNull(schemaType),
+                    ActionConstants.ACTION_TYPE_CLICK);
+        }
+
+        /**
+         * Creates a new {@link ClickActionGenericDocument.Builder} from an existing {@link
+         * GenericDocument}.
+         *
+         * @param document a generic document object.
+         * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+         *     not {@link ActionConstants#ACTION_TYPE_CLICK}.
+         */
+        public Builder(@NonNull GenericDocument document) {
+            super(Objects.requireNonNull(document));
+
+            if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)
+                    != ActionConstants.ACTION_TYPE_CLICK) {
+                throw new IllegalArgumentException(
+                        "Invalid action type for ClickActionGenericDocument");
+            }
+        }
+
+        /**
+         * Sets the string value of property {@code query} by the user-entered search input (without
+         * any operators or rewriting) that yielded the {@link android.app.appsearch.SearchResult}
+         * on which the user clicked.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQuery(@NonNull String query) {
+            Objects.requireNonNull(query);
+            setPropertyString(PROPERTY_PATH_QUERY, query);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code resultRankInBlock} by the rank of the clicked
+         * {@link android.app.appsearch.SearchResult} document among the user-defined block.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankInBlock(int resultRankInBlock) {
+            Preconditions.checkArgumentNonnegative(resultRankInBlock);
+            setPropertyLong(PROPERTY_PATH_RESULT_RANK_IN_BLOCK, resultRankInBlock);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code resultRankGlobal} by the global rank of the
+         * clicked {@link android.app.appsearch.SearchResult} document.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setResultRankGlobal(int resultRankGlobal) {
+            Preconditions.checkArgumentNonnegative(resultRankGlobal);
+            setPropertyLong(PROPERTY_PATH_RESULT_RANK_GLOBAL, resultRankGlobal);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code timeStayOnResultMillis} by the time in
+         * milliseconds that user stays on the {@link android.app.appsearch.SearchResult} document
+         * after clicking it.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) {
+            setPropertyLong(PROPERTY_PATH_TIME_STAY_ON_RESULT_MILLIS, timeStayOnResultMillis);
+            return this;
+        }
+
+        /** Builds a {@link ClickActionGenericDocument}. */
+        @Override
+        @NonNull
+        public ClickActionGenericDocument build() {
+            return new ClickActionGenericDocument(super.build());
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/usagereporting/SearchActionGenericDocument.java b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/SearchActionGenericDocument.java
new file mode 100644
index 0000000..aa653db
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/SearchActionGenericDocument.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.usagereporting.ActionConstants;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Wrapper class for
+ *
+ * <p>search action
+ *
+ * <p>{@link GenericDocument}, which contains getters for search action properties.
+ *
+ * @hide
+ */
+public class SearchActionGenericDocument extends TakenActionGenericDocument {
+    private static final String PROPERTY_PATH_QUERY = "query";
+    private static final String PROPERTY_PATH_FETCHED_RESULT_COUNT = "fetchedResultCount";
+
+    SearchActionGenericDocument(@NonNull GenericDocument document) {
+        super(Objects.requireNonNull(document));
+    }
+
+    /** Returns the string value of property {@code query}. */
+    @Nullable
+    public String getQuery() {
+        return getPropertyString(PROPERTY_PATH_QUERY);
+    }
+
+    /** Returns the integer value of property {@code fetchedResultCount}. */
+    public int getFetchedResultCount() {
+        return (int) getPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT);
+    }
+
+    /** Builder for {@link SearchActionGenericDocument}. */
+    public static final class Builder extends TakenActionGenericDocument.Builder<Builder> {
+        /**
+         * Creates a new {@link SearchActionGenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace the namespace to set for the {@link GenericDocument}.
+         * @param id the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *     provided {@code schemaType} must be defined using {@link AppSearchSession#setSchema}
+         *     prior to inserting a document of this {@code schemaType} into the AppSearch index
+         *     using {@link AppSearchSession#put}. Otherwise, the document will be rejected by
+         *     {@link AppSearchSession#put} with result code {@link
+         *     AppSearchResult#RESULT_NOT_FOUND}.
+         */
+        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
+            super(
+                    Objects.requireNonNull(namespace),
+                    Objects.requireNonNull(id),
+                    Objects.requireNonNull(schemaType),
+                    ActionConstants.ACTION_TYPE_SEARCH);
+        }
+
+        /**
+         * Creates a new {@link SearchActionGenericDocument.Builder} from an existing {@link
+         * GenericDocument}.
+         *
+         * @param document a generic document object.
+         * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+         *     not {@link ActionConstants#ACTION_TYPE_SEARCH}.
+         */
+        public Builder(@NonNull GenericDocument document) {
+            super(Objects.requireNonNull(document));
+
+            if (document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE)
+                    != ActionConstants.ACTION_TYPE_SEARCH) {
+                throw new IllegalArgumentException(
+                        "Invalid action type for SearchActionGenericDocument");
+            }
+        }
+
+        /**
+         * Sets the string value of property {@code query} by the user-entered search input (without
+         * any operators or rewriting).
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setQuery(@NonNull String query) {
+            Objects.requireNonNull(query);
+            setPropertyString(PROPERTY_PATH_QUERY, query);
+            return this;
+        }
+
+        /**
+         * Sets the integer value of property {@code fetchedResultCount} by total number of results
+         * fetched from AppSearch by the client in this search action.
+         */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setFetchedResultCount(int fetchedResultCount) {
+            Preconditions.checkArgumentNonnegative(fetchedResultCount);
+            setPropertyLong(PROPERTY_PATH_FETCHED_RESULT_COUNT, fetchedResultCount);
+            return this;
+        }
+
+        /** Builds a {@link SearchActionGenericDocument}. */
+        @Override
+        @NonNull
+        public SearchActionGenericDocument build() {
+            return new SearchActionGenericDocument(super.build());
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/usagereporting/SearchSessionStatsExtractor.java b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/SearchSessionStatsExtractor.java
new file mode 100644
index 0000000..7f5dcab
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/SearchSessionStatsExtractor.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.usagereporting.ActionConstants;
+
+import com.android.server.appsearch.external.localstorage.stats.ClickStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchIntentStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchSessionStats;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Extractor class for analyzing a list of taken action {@link GenericDocument} and creating a list
+ * of {@link SearchSessionStats}.
+ *
+ * @hide
+ */
+public final class SearchSessionStatsExtractor {
+    // TODO(b/319285816): make thresholds configurable.
+    /**
+     * Threshold for noise search intent detection, in millisecond. A search action will be
+     * considered as a noise (and skipped) if all of the following conditions are satisfied:
+     *
+     * <ul>
+     *   <li>The action timestamp (action document creation timestamp) difference between it and its
+     *       previous search action is below this threshold.
+     *   <li>There is no click action associated with it.
+     *   <li>Its raw query string is a prefix of the previous search action's raw query string (or
+     *       the other way around).
+     * </ul>
+     */
+    private static final long NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS = 2000L;
+
+    /**
+     * Threshold for independent search intent detection, in millisecond. If the action timestamp
+     * (action document creation timestamp) difference between the previous and the current search
+     * action exceeds this threshold, then the current search action will be considered as a
+     * completely independent search intent (i.e. belonging to a new search session), and there will
+     * be no correlation analysis between the previous and the current search action.
+     */
+    private static final long INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS =
+            10L * 60 * 1000;
+
+    /**
+     * Threshold for marking good click (compared with {@code timeStayOnResultMillis}), in
+     * millisecond. A good click means the user spent decent amount of time on the clicked result
+     * document.
+     */
+    private static final long GOOD_CLICK_TIME_STAY_ON_RESULT_THRESHOLD_MILLIS = 2000L;
+
+    /**
+     * Threshold for backspace count to become query abandonment. If the user hits backspace for at
+     * least QUERY_ABANDONMENT_BACKSPACE_COUNT times, then the query correction type will be
+     * determined as abandonment.
+     */
+    private static final int QUERY_ABANDONMENT_BACKSPACE_COUNT = 2;
+
+    /**
+     * Returns the query correction type between the previous and current search actions.
+     *
+     * @param currSearchAction the current search action {@link SearchActionGenericDocument}.
+     * @param prevSearchAction the previous search action {@link SearchActionGenericDocument}.
+     */
+    public static @SearchIntentStats.QueryCorrectionType int getQueryCorrectionType(
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @Nullable SearchActionGenericDocument prevSearchAction) {
+        Objects.requireNonNull(currSearchAction);
+
+        if (currSearchAction.getQuery() == null) {
+            // Query correction type cannot be determined if the client didn't provide the raw query
+            // string.
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;
+        }
+        if (prevSearchAction == null) {
+            // If the previous search action is missing, then it is the first query.
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY;
+        } else if (prevSearchAction.getQuery() == null) {
+            // Query correction type cannot be determined if the client didn't provide the raw query
+            // string.
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN;
+        }
+
+        // Determine the query correction type by comparing the current and previous raw query
+        // strings.
+        String prevQuery = prevSearchAction.getQuery();
+        String currQuery = currSearchAction.getQuery();
+        int commonPrefixLength = getCommonPrefixLength(prevQuery, currQuery);
+        // If the user hits backspace >= QUERY_ABANDONMENT_BACKSPACE_COUNT times, then it is query
+        // abandonment. Otherwise, it is query refinement.
+        if (commonPrefixLength <= prevQuery.length() - QUERY_ABANDONMENT_BACKSPACE_COUNT) {
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+        } else {
+            return SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT;
+        }
+    }
+
+    /**
+     * Returns a list of {@link SearchSessionStats} extracted from the given list of taken action
+     * {@link GenericDocument}.
+     *
+     * <p>A search session consists of several related search intents.
+     *
+     * <p>A search intent consists of a valid search action with 0 or more click actions. To extract
+     * search intent metrics, this function will try to group the given taken actions into several
+     * search intents, and yield a {@link SearchIntentStats} for each search intent. Finally related
+     * {@link SearchIntentStats} will be wrapped into {@link SearchSessionStats}.
+     *
+     * @param packageName The package name of the caller.
+     * @param database The database name of the caller.
+     * @param genericDocuments a list of taken actions in generic document form.
+     */
+    @NonNull
+    public List<SearchSessionStats> extract(
+            @NonNull String packageName,
+            @Nullable String database,
+            @NonNull List<GenericDocument> genericDocuments) {
+        Objects.requireNonNull(genericDocuments);
+
+        // Convert GenericDocument list to TakenActionGenericDocument list and sort them by document
+        // creation timestamp.
+        List<TakenActionGenericDocument> takenActionGenericDocuments =
+                new ArrayList<>(genericDocuments.size());
+        for (int i = 0; i < genericDocuments.size(); ++i) {
+            try {
+                takenActionGenericDocuments.add(
+                        TakenActionGenericDocument.create(genericDocuments.get(i)));
+            } catch (IllegalArgumentException e) {
+                // Skip generic documents with unknown action type.
+            }
+        }
+        Collections.sort(
+                takenActionGenericDocuments,
+                (TakenActionGenericDocument doc1, TakenActionGenericDocument doc2) ->
+                        Long.compare(
+                                doc1.getCreationTimestampMillis(),
+                                doc2.getCreationTimestampMillis()));
+
+        List<SearchSessionStats> result = new ArrayList<>();
+        SearchSessionStats.Builder searchSessionStatsBuilder = null;
+        SearchActionGenericDocument prevSearchAction = null;
+        // Clients are expected to report search action followed by its associated click actions.
+        // For example, [searchAction1, clickAction1, searchAction2, searchAction3, clickAction2,
+        // clickAction3]:
+        // - There are 3 search actions and 3 click actions.
+        // - clickAction1 is associated with searchAction1.
+        // - There is no click action associated with searchAction2.
+        // - clickAction2 and clickAction3 are associated with searchAction3.
+        // Here we're going to break down the list into segments. Each segment starts with a search
+        // action followed by 0 or more associated click actions, and they form a single search
+        // intent. We will analyze and extract metrics from the taken actions for the search intent.
+        //
+        // If a search intent is considered independent from the previous one, then we will start a
+        // new search session analysis.
+        for (int i = 0; i < takenActionGenericDocuments.size(); ++i) {
+            if (takenActionGenericDocuments.get(i).getActionType()
+                    != ActionConstants.ACTION_TYPE_SEARCH) {
+                continue;
+            }
+
+            SearchActionGenericDocument currSearchAction =
+                    (SearchActionGenericDocument) takenActionGenericDocuments.get(i);
+            List<ClickActionGenericDocument> clickActions = new ArrayList<>();
+            // Get all click actions associated with the current search action by advancing until
+            // the next search action.
+            while (i + 1 < takenActionGenericDocuments.size()
+                    && takenActionGenericDocuments.get(i + 1).getActionType()
+                            != ActionConstants.ACTION_TYPE_SEARCH) {
+                if (takenActionGenericDocuments.get(i + 1).getActionType()
+                        == ActionConstants.ACTION_TYPE_CLICK) {
+                    clickActions.add(
+                            (ClickActionGenericDocument) takenActionGenericDocuments.get(i + 1));
+                }
+                ++i;
+            }
+
+            // Get the reference of the next search action if it exists.
+            SearchActionGenericDocument nextSearchAction = null;
+            if (i + 1 < takenActionGenericDocuments.size()
+                    && takenActionGenericDocuments.get(i + 1).getActionType()
+                            == ActionConstants.ACTION_TYPE_SEARCH) {
+                nextSearchAction =
+                        (SearchActionGenericDocument) takenActionGenericDocuments.get(i + 1);
+            }
+
+            if (prevSearchAction != null
+                    && isIndependentSearchAction(currSearchAction, prevSearchAction)) {
+                // If the current search action is independent from the previous one, then:
+                // - Build and append the previous search session stats.
+                // - Start a new search session analysis.
+                // - Ignore the previous search action when extracting stats.
+                if (searchSessionStatsBuilder != null) {
+                    result.add(searchSessionStatsBuilder.build());
+                    searchSessionStatsBuilder = null;
+                }
+                prevSearchAction = null;
+            } else if (clickActions.isEmpty()
+                    && isIntermediateSearchAction(
+                            currSearchAction, prevSearchAction, nextSearchAction)) {
+                // If the current search action is an intermediate search action with no click
+                // actions, then we consider it as a noise and skip it.
+                continue;
+            }
+
+            // Now we get a valid search intent (the current search action + a list of click actions
+            // associated with it). Extract metrics and add SearchIntentStats into this search
+            // session.
+            if (searchSessionStatsBuilder == null) {
+                searchSessionStatsBuilder =
+                        new SearchSessionStats.Builder(packageName).setDatabase(database);
+            }
+            searchSessionStatsBuilder.addSearchIntentsStats(
+                    createSearchIntentStats(
+                            packageName,
+                            database,
+                            currSearchAction,
+                            clickActions,
+                            prevSearchAction));
+            prevSearchAction = currSearchAction;
+        }
+        if (searchSessionStatsBuilder != null) {
+            result.add(searchSessionStatsBuilder.build());
+        }
+        return result;
+    }
+
+    /**
+     * Creates a {@link SearchIntentStats} object from the current search action + its associated
+     * click actions, and the previous search action (in generic document form).
+     */
+    private SearchIntentStats createSearchIntentStats(
+            @NonNull String packageName,
+            @Nullable String database,
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @NonNull List<ClickActionGenericDocument> clickActions,
+            @Nullable SearchActionGenericDocument prevSearchAction) {
+        SearchIntentStats.Builder builder =
+                new SearchIntentStats.Builder(packageName)
+                        .setDatabase(database)
+                        .setTimestampMillis(currSearchAction.getCreationTimestampMillis())
+                        .setCurrQuery(currSearchAction.getQuery())
+                        .setNumResultsFetched(currSearchAction.getFetchedResultCount())
+                        .setQueryCorrectionType(
+                                getQueryCorrectionType(currSearchAction, prevSearchAction));
+        if (prevSearchAction != null) {
+            builder.setPrevQuery(prevSearchAction.getQuery());
+        }
+        for (int i = 0; i < clickActions.size(); ++i) {
+            builder.addClicksStats(createClickStats(clickActions.get(i)));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Creates a {@link ClickStats} object from the given click action (in generic document form).
+     */
+    private ClickStats createClickStats(ClickActionGenericDocument clickAction) {
+        // A click is considered good if:
+        // - The user spent decent amount of time on the clicked document.
+        // - OR the client didn't provide timeStayOnResultMillis. In this case, the value will be 0.
+        boolean isGoodClick =
+                clickAction.getTimeStayOnResultMillis() <= 0
+                        || clickAction.getTimeStayOnResultMillis()
+                                >= GOOD_CLICK_TIME_STAY_ON_RESULT_THRESHOLD_MILLIS;
+        return new ClickStats.Builder()
+                .setTimestampMillis(clickAction.getCreationTimestampMillis())
+                .setResultRankInBlock(clickAction.getResultRankInBlock())
+                .setResultRankGlobal(clickAction.getResultRankGlobal())
+                .setTimeStayOnResultMillis(clickAction.getTimeStayOnResultMillis())
+                .setIsGoodClick(isGoodClick)
+                .build();
+    }
+
+    /**
+     * Returns if the current search action is an intermediate search action.
+     *
+     * <p>An intermediate search action is used for detecting the situation when the user adds or
+     * deletes characters from the query (e.g. "a" -> "app" -> "apple" or "apple" -> "app" -> "a")
+     * within a short period of time. More precisely, it has to satisfy all of the following
+     * conditions:
+     *
+     * <ul>
+     *   <li>There are related (non-independent) search actions before and after it.
+     *   <li>It occurs within the threshold after its previous search action.
+     *   <li>Its raw query string is a prefix of its previous search action's raw query string, or
+     *       the opposite direction.
+     * </ul>
+     */
+    private static boolean isIntermediateSearchAction(
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @Nullable SearchActionGenericDocument prevSearchAction,
+            @Nullable SearchActionGenericDocument nextSearchAction) {
+        Objects.requireNonNull(currSearchAction);
+
+        if (prevSearchAction == null || nextSearchAction == null) {
+            return false;
+        }
+
+        // Whether the next search action is independent from the current search action. If true,
+        // then the current search action will not be considered as an intermediate search action
+        // since it is the last search action of the search session.
+        boolean isNextSearchActionIndependent =
+                isIndependentSearchAction(nextSearchAction, currSearchAction);
+
+        // Whether the current search action occurs within the threshold after the previous search
+        // action.
+        boolean occursWithinTimeThreshold =
+                currSearchAction.getCreationTimestampMillis()
+                                - prevSearchAction.getCreationTimestampMillis()
+                        <= NOISE_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;
+
+        // Whether the previous search action's raw query string is a prefix of the current search
+        // action's, or the opposite direction (e.g. "app" -> "apple" and "apple" -> "app").
+        String prevQuery = prevSearchAction.getQuery();
+        String currQuery = currSearchAction.getQuery();
+        boolean isPrefix =
+                prevQuery != null
+                        && currQuery != null
+                        && (currQuery.startsWith(prevQuery) || prevQuery.startsWith(currQuery));
+
+        return !isNextSearchActionIndependent && occursWithinTimeThreshold && isPrefix;
+    }
+
+    /**
+     * Returns if the current search action is independent from the previous search action.
+     *
+     * <p>If the current search action occurs later than the threshold after the previous search
+     * action, then they are considered independent.
+     */
+    private static boolean isIndependentSearchAction(
+            @NonNull SearchActionGenericDocument currSearchAction,
+            @NonNull SearchActionGenericDocument prevSearchAction) {
+        Objects.requireNonNull(currSearchAction);
+        Objects.requireNonNull(prevSearchAction);
+
+        long searchTimeDiffMillis =
+                currSearchAction.getCreationTimestampMillis()
+                        - prevSearchAction.getCreationTimestampMillis();
+        return searchTimeDiffMillis > INDEPENDENT_SEARCH_INTENT_TIMESTAMP_DIFF_THRESHOLD_MILLIS;
+    }
+
+    /** Returns the common prefix length of the given 2 strings. */
+    private static int getCommonPrefixLength(@NonNull String s1, @NonNull String s2) {
+        Objects.requireNonNull(s1);
+        Objects.requireNonNull(s2);
+
+        int minLength = Math.min(s1.length(), s2.length());
+        for (int i = 0; i < minLength; ++i) {
+            if (s1.charAt(i) != s2.charAt(i)) {
+                return i;
+            }
+        }
+        return minLength;
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/usagereporting/TakenActionGenericDocument.java b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/TakenActionGenericDocument.java
new file mode 100644
index 0000000..8e03a54
--- /dev/null
+++ b/service/java/com/android/server/appsearch/external/localstorage/usagereporting/TakenActionGenericDocument.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.usagereporting.ActionConstants;
+
+import java.util.Objects;
+
+/**
+ * Abstract wrapper class for {@link GenericDocument} of all types of taken actions, which contains
+ * common getters and constants.
+ *
+ * @hide
+ */
+public abstract class TakenActionGenericDocument extends GenericDocument {
+    protected static final String PROPERTY_PATH_ACTION_TYPE = "actionType";
+
+    /**
+     * Static factory method to create a concrete object of {@link TakenActionGenericDocument} child
+     * type, according to the given {@link GenericDocument}'s action type.
+     *
+     * @param document a generic document object.
+     * @throws IllegalArgumentException if the integer value of property {@code actionType} is
+     *     invalid.
+     */
+    @NonNull
+    public static TakenActionGenericDocument create(@NonNull GenericDocument document)
+            throws IllegalArgumentException {
+        Objects.requireNonNull(document);
+        int actionType = (int) document.getPropertyLong(PROPERTY_PATH_ACTION_TYPE);
+        switch (actionType) {
+            case ActionConstants.ACTION_TYPE_SEARCH:
+                return new SearchActionGenericDocument.Builder(document).build();
+            case ActionConstants.ACTION_TYPE_CLICK:
+                return new ClickActionGenericDocument.Builder(document).build();
+            default:
+                throw new IllegalArgumentException(
+                        "Cannot create taken action generic document with unknown action type");
+        }
+    }
+
+    protected TakenActionGenericDocument(@NonNull GenericDocument document) {
+        super(Objects.requireNonNull(document));
+    }
+
+    /** Returns the (enum) integer value of property {@code actionType}. */
+    public int getActionType() {
+        return (int) getPropertyLong(PROPERTY_PATH_ACTION_TYPE);
+    }
+
+    /** Abstract builder for {@link TakenActionGenericDocument}. */
+    abstract static class Builder<T extends Builder<T>> extends GenericDocument.Builder<T> {
+        /**
+         * Creates a new {@link TakenActionGenericDocument.Builder}.
+         *
+         * <p>Document IDs are unique within a namespace.
+         *
+         * <p>The number of namespaces per app should be kept small for efficiency reasons.
+         *
+         * @param namespace the namespace to set for the {@link GenericDocument}.
+         * @param id the unique identifier for the {@link GenericDocument} in its namespace.
+         * @param schemaType the {@link AppSearchSchema} type of the {@link GenericDocument}. The
+         *     provided {@code schemaType} must be defined using {@link AppSearchSession#setSchema}
+         *     prior to inserting a document of this {@code schemaType} into the AppSearch index
+         *     using {@link AppSearchSession#put}. Otherwise, the document will be rejected by
+         *     {@link AppSearchSession#put} with result code {@link
+         *     AppSearchResult#RESULT_NOT_FOUND}.
+         * @param actionType the action type of the taken action. See definitions in {@link
+         *     ActionConstants}.
+         */
+        Builder(
+                @NonNull String namespace,
+                @NonNull String id,
+                @NonNull String schemaType,
+                int actionType) {
+            super(
+                    Objects.requireNonNull(namespace),
+                    Objects.requireNonNull(id),
+                    Objects.requireNonNull(schemaType));
+
+            setPropertyLong(PROPERTY_PATH_ACTION_TYPE, actionType);
+        }
+
+        /**
+         * Creates a new {@link TakenActionGenericDocument.Builder} from an existing {@link
+         * GenericDocument}.
+         */
+        Builder(@NonNull GenericDocument document) {
+            super(Objects.requireNonNull(document));
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java
index 8c23a81..4fa5a78 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/CallerAccess.java
@@ -51,8 +51,12 @@
 
     @Override
     public boolean equals(@Nullable Object o) {
-        if (this == o) return true;
-        if (!(o instanceof CallerAccess)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof CallerAccess)) {
+            return false;
+        }
         CallerAccess that = (CallerAccess) o;
         return mCallingPackageName.equals(that.mCallingPackageName);
     }
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityDocumentV1.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityDocumentV1.java
index 662ceb5..0f146ce 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityDocumentV1.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityDocumentV1.java
@@ -34,6 +34,7 @@
 class VisibilityDocumentV1 extends GenericDocument {
     /** The Schema type for documents that hold AppSearch's metadata, e.g. visibility settings. */
     static final String SCHEMA_TYPE = "VisibilityType";
+
     /** Namespace of documents that contain visibility settings */
     static final String NAMESPACE = "";
 
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
index 9d60a23..8b8b1b5 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStore.java
@@ -26,6 +26,9 @@
 import android.app.appsearch.InternalSetSchemaResponse;
 import android.app.appsearch.InternalVisibilityConfig;
 import android.app.appsearch.VisibilityPermissionConfig;
+import android.app.appsearch.checker.initialization.qual.UnderInitialization;
+import android.app.appsearch.checker.initialization.qual.UnknownInitialization;
+import android.app.appsearch.checker.nullness.qual.RequiresNonNull;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.app.appsearch.util.LogUtil;
 import android.util.ArrayMap;
@@ -60,6 +63,7 @@
  */
 public class VisibilityStore {
     private static final String TAG = "AppSearchVisibilityStor";
+
     /**
      * These cannot have any of the special characters used by AppSearchImpl (e.g. {@code
      * AppSearchImpl#PACKAGE_DELIMITER} or {@code AppSearchImpl#DATABASE_DELIMITER}.
@@ -84,7 +88,7 @@
                 mAppSearchImpl.getSchema(
                         VISIBILITY_PACKAGE_NAME,
                         VISIBILITY_DATABASE_NAME,
-                        new CallerAccess(/*callingPackageName=*/ VISIBILITY_PACKAGE_NAME));
+                        new CallerAccess(/* callingPackageName= */ VISIBILITY_PACKAGE_NAME));
         List<VisibilityDocumentV1> visibilityDocumentsV1s = null;
         switch (getSchemaResponse.getVersion()) {
             case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_PACKAGE:
@@ -148,8 +152,8 @@
                     VISIBILITY_DATABASE_NAME,
                     VisibilityToDocumentConverter.createVisibilityDocument(
                             prefixedVisibilityConfig),
-                    /*sendChangeNotifications=*/ false,
-                    /*logger=*/ null);
+                    /* sendChangeNotifications= */ false,
+                    /* logger= */ null);
 
             // Put the android V visibility overlay document to AppSearchImpl.
             GenericDocument androidVOverlay =
@@ -159,8 +163,8 @@
                         VISIBILITY_PACKAGE_NAME,
                         ANDROID_V_OVERLAY_DATABASE_NAME,
                         androidVOverlay,
-                        /*sendChangeNotifications=*/ false,
-                        /*logger=*/ null);
+                        /* sendChangeNotifications= */ false,
+                        /* logger= */ null);
             } else if (isConfigContainsAndroidVOverlay(oldVisibilityConfig)) {
                 // We need to make sure to remove the VisibilityOverlay on disk as the current
                 // VisibilityConfig does not have a VisibilityOverlay.
@@ -172,7 +176,7 @@
                             ANDROID_V_OVERLAY_DATABASE_NAME,
                             VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
                             prefixedVisibilityConfig.getSchemaType(),
-                            /*removeStatsBuilder=*/ null);
+                            /* removeStatsBuilder= */ null);
                 } catch (AppSearchException e) {
                     // If it already doesn't exist, that is fine
                     if (e.getResultCode() != RESULT_NOT_FOUND) {
@@ -205,7 +209,7 @@
                             VISIBILITY_DATABASE_NAME,
                             VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                             prefixedSchemaType,
-                            /*removeStatsBuilder=*/ null);
+                            /* removeStatsBuilder= */ null);
                 } catch (AppSearchException e) {
                     if (e.getResultCode() == RESULT_NOT_FOUND) {
                         // We are trying to remove this visibility setting, so it's weird but seems
@@ -226,7 +230,7 @@
                             ANDROID_V_OVERLAY_DATABASE_NAME,
                             VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
                             prefixedSchemaType,
-                            /*removeStatsBuilder=*/ null);
+                            /* removeStatsBuilder= */ null);
                 } catch (AppSearchException e) {
                     if (e.getResultCode() == RESULT_NOT_FOUND) {
                         // It's possible no overlay was set, so this this is fine.
@@ -255,7 +259,9 @@
      * Loads all stored latest {@link InternalVisibilityConfig} from Icing, and put them into {@link
      * #mVisibilityConfigMap}.
      */
-    private void loadVisibilityConfigMap() throws AppSearchException {
+    @RequiresNonNull("mAppSearchImpl")
+    private void loadVisibilityConfigMap(@UnderInitialization VisibilityStore this)
+            throws AppSearchException {
         // Populate visibility settings set
         List<String> cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes();
         for (int i = 0; i < cachedSchemaTypes.size(); i++) {
@@ -274,8 +280,8 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefixedSchemaType,
-                                /*typePropertyPaths=*/ Collections.emptyMap());
+                                /* id= */ prefixedSchemaType,
+                                /* typePropertyPaths= */ Collections.emptyMap());
             } catch (AppSearchException e) {
                 if (e.getResultCode() == RESULT_NOT_FOUND) {
                     // The schema has all default setting and we won't have a VisibilityDocument for
@@ -292,8 +298,8 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 ANDROID_V_OVERLAY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                                /*id=*/ prefixedSchemaType,
-                                /*typePropertyPaths=*/ Collections.emptyMap());
+                                /* id= */ prefixedSchemaType,
+                                /* typePropertyPaths= */ Collections.emptyMap());
             } catch (AppSearchException e) {
                 if (e.getResultCode() != RESULT_NOT_FOUND) {
                     // This is some other error we should pass up.
@@ -311,8 +317,11 @@
     }
 
     /** Set the latest version of {@link InternalVisibilityConfig} and its schema to AppSearch. */
+    @RequiresNonNull("mAppSearchImpl")
     private void setLatestSchemaAndDocuments(
-            @NonNull List<InternalVisibilityConfig> migratedDocuments) throws AppSearchException {
+            @UnderInitialization VisibilityStore this,
+            @NonNull List<InternalVisibilityConfig> migratedDocuments)
+            throws AppSearchException {
         // The latest schema type doesn't exist yet. Add it. Set forceOverride true to
         // delete old schema.
         InternalSetSchemaResponse internalSetSchemaResponse =
@@ -322,10 +331,10 @@
                         Arrays.asList(
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
                                 VisibilityPermissionConfig.SCHEMA),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                        /* setSchemaStatsBuilder= */ null);
         if (!internalSetSchemaResponse.isSuccess()) {
             // Impossible case, we just set forceOverride to be true, we should never
             // fail in incompatible changes.
@@ -339,11 +348,11 @@
                         ANDROID_V_OVERLAY_DATABASE_NAME,
                         Collections.singletonList(
                                 VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ VisibilityToDocumentConverter
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ VisibilityToDocumentConverter
                                 .ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* setSchemaStatsBuilder= */ null);
         if (!internalSetAndroidVOverlaySchemaResponse.isSuccess()) {
             // Impossible case, we just set forceOverride to be true, we should never
             // fail in incompatible changes.
@@ -358,8 +367,8 @@
                     VISIBILITY_PACKAGE_NAME,
                     VISIBILITY_DATABASE_NAME,
                     VisibilityToDocumentConverter.createVisibilityDocument(migratedConfig),
-                    /*sendChangeNotifications=*/ false,
-                    /*logger=*/ null);
+                    /* sendChangeNotifications= */ false,
+                    /* logger= */ null);
         }
     }
 
@@ -367,12 +376,14 @@
      * Check and migrate visibility schemas in {@link #ANDROID_V_OVERLAY_DATABASE_NAME} to {@link
      * VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST}.
      */
-    private void migrateVisibilityOverlayDatabase() throws AppSearchException {
+    @RequiresNonNull("mAppSearchImpl")
+    private void migrateVisibilityOverlayDatabase(@UnderInitialization VisibilityStore this)
+            throws AppSearchException {
         GetSchemaResponse getSchemaResponse =
                 mAppSearchImpl.getSchema(
                         VISIBILITY_PACKAGE_NAME,
                         ANDROID_V_OVERLAY_DATABASE_NAME,
-                        new CallerAccess(/*callingPackageName=*/ VISIBILITY_PACKAGE_NAME));
+                        new CallerAccess(/* callingPackageName= */ VISIBILITY_PACKAGE_NAME));
         switch (getSchemaResponse.getVersion()) {
             case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG:
                 // Force override to next version. This version hasn't released to any public
@@ -384,11 +395,11 @@
                                 ANDROID_V_OVERLAY_DATABASE_NAME,
                                 Collections.singletonList(
                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
-                                /*visibilityConfigs=*/ Collections.emptyList(),
-                                /*forceOverride=*/ true, // force update to nest version.
+                                /* visibilityConfigs= */ Collections.emptyList(),
+                                /* forceOverride= */ true, // force update to nest version.
                                 VisibilityToDocumentConverter
                                         .ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
-                                /*setSchemaStatsBuilder=*/ null);
+                                /* setSchemaStatsBuilder= */ null);
                 if (!internalSetSchemaResponse.isSuccess()) {
                     // Impossible case, we just set forceOverride to be true, we should never
                     // fail in incompatible changes.
@@ -411,7 +422,9 @@
     /**
      * Verify the existing visibility schema, set the latest visibilility schema if it's missing.
      */
-    private void verifyOrSetLatestVisibilitySchema(@NonNull GetSchemaResponse getSchemaResponse)
+    @RequiresNonNull("mAppSearchImpl")
+    private void verifyOrSetLatestVisibilitySchema(
+            @UnderInitialization VisibilityStore this, @NonNull GetSchemaResponse getSchemaResponse)
             throws AppSearchException {
         // We cannot change the schema version past 2 as detecting version "3" would hit the
         // default block and throw an AppSearchException. This is why we added
@@ -436,10 +449,10 @@
                             Arrays.asList(
                                     VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
                                     VisibilityPermissionConfig.SCHEMA),
-                            /*visibilityConfigs=*/ Collections.emptyList(),
-                            /*forceOverride=*/ true,
-                            /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
-                            /*setSchemaStatsBuilder=*/ null);
+                            /* visibilityConfigs= */ Collections.emptyList(),
+                            /* forceOverride= */ true,
+                            /* version= */ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                            /* setSchemaStatsBuilder= */ null);
             if (!internalSetSchemaResponse.isSuccess()) {
                 throw new AppSearchException(
                         AppSearchResult.RESULT_INTERNAL_ERROR,
@@ -457,10 +470,10 @@
                             Arrays.asList(
                                     VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
                                     VisibilityPermissionConfig.SCHEMA),
-                            /*visibilityConfigs=*/ Collections.emptyList(),
-                            /*forceOverride=*/ false,
-                            /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
-                            /*setSchemaStatsBuilder=*/ null);
+                            /* visibilityConfigs= */ Collections.emptyList(),
+                            /* forceOverride= */ false,
+                            /* version= */ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                            /* setSchemaStatsBuilder= */ null);
             if (!internalSetSchemaResponse.isSuccess()) {
                 // If you hit problem here it means you made a incompatible change in
                 // Visibility Schema without update the version number. You should bump
@@ -479,8 +492,11 @@
     /**
      * Verify the existing visibility overlay schema, set the latest overlay schema if it's missing.
      */
+    @RequiresNonNull("mAppSearchImpl")
     private void verifyOrSetLatestVisibilityOverlaySchema(
-            @NonNull GetSchemaResponse getAndroidVOverlaySchemaResponse) throws AppSearchException {
+            @UnknownInitialization VisibilityStore this,
+            @NonNull GetSchemaResponse getAndroidVOverlaySchemaResponse)
+            throws AppSearchException {
         // Check Android V overlay schema.
         Set<AppSearchSchema> existingAndroidVOverlaySchema =
                 getAndroidVOverlaySchemaResponse.getSchemas();
@@ -494,10 +510,10 @@
                             ANDROID_V_OVERLAY_DATABASE_NAME,
                             Collections.singletonList(
                                     VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA),
-                            /*visibilityConfigs=*/ Collections.emptyList(),
-                            /*forceOverride=*/ false,
+                            /* visibilityConfigs= */ Collections.emptyList(),
+                            /* forceOverride= */ false,
                             VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
-                            /*setSchemaStatsBuilder=*/ null);
+                            /* setSchemaStatsBuilder= */ null);
             if (!internalSetSchemaResponse.isSuccess()) {
                 // If you hit problem here it means you made a incompatible change in
                 // Visibility Schema. You should create new overlay schema
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
index 2b40ab6..30a88d4 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
@@ -44,6 +44,7 @@
  */
 public class VisibilityStoreMigrationHelperFromV0 {
     private VisibilityStoreMigrationHelperFromV0() {}
+
     /** Prefix to add to all visibility document ids. IcingSearchEngine doesn't allow empty ids. */
     private static final String DEPRECATED_ID_PREFIX = "uri:";
 
@@ -149,7 +150,7 @@
                                     VisibilityStore.VISIBILITY_DATABASE_NAME,
                                     VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                                     getDeprecatedVisibilityDocumentId(packageName, databaseName),
-                                    /*typePropertyPaths=*/ Collections.emptyMap()));
+                                    /* typePropertyPaths= */ Collections.emptyMap()));
                 } catch (AppSearchException e) {
                     if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
                         // TODO(b/172068212): This indicates some desync error. We were expecting a
@@ -252,7 +253,7 @@
             @NonNull String schemaType) {
         VisibilityDocumentV1.Builder builder = documentBuilderMap.get(schemaType);
         if (builder == null) {
-            builder = new VisibilityDocumentV1.Builder(/*id=*/ schemaType);
+            builder = new VisibilityDocumentV1.Builder(/* id= */ schemaType);
             documentBuilderMap.put(schemaType, builder);
         }
         return builder;
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
index 03dd559..ec87a95 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
@@ -68,7 +68,7 @@
                                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                                         allPrefixedSchemaTypes.get(i),
-                                        /*typePropertyPaths=*/ Collections.emptyMap())));
+                                        /* typePropertyPaths= */ Collections.emptyMap())));
             } catch (AppSearchException e) {
                 if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
                     // TODO(b/172068212): This indicates some desync error. We were expecting a
diff --git a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverter.java b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverter.java
index 005c31d..5d27c0c 100644
--- a/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverter.java
+++ b/service/java/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverter.java
@@ -50,6 +50,7 @@
      * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
      */
     public static final String VISIBILITY_DOCUMENT_SCHEMA_TYPE = "VisibilityType";
+
     /** Namespace of documents that contain visibility settings */
     public static final String VISIBILITY_DOCUMENT_NAMESPACE = "";
 
@@ -58,8 +59,10 @@
      * additional visibility settings.
      */
     public static final String ANDROID_V_OVERLAY_SCHEMA_TYPE = "AndroidVOverlayType";
+
     /** Namespace of documents that contain Android V visibility setting overlay documents */
     public static final String ANDROID_V_OVERLAY_NAMESPACE = "androidVOverlay";
+
     /** Property that holds the serialized {@link AndroidVOverlayProto}. */
     public static final String VISIBILITY_PROTO_SERIALIZE_PROPERTY =
             "visibilityProtoSerializeProperty";
@@ -147,6 +150,7 @@
                                             AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                     .build())
                     .build();
+
     /**
      * The Deprecated schemas and properties that we need to remove from visibility database.
      * TODO(b/321326441) remove this method when we no longer to migrate devices in this state.
diff --git a/service/java/com/android/server/appsearch/indexer/IndexerLocalService.java b/service/java/com/android/server/appsearch/indexer/IndexerLocalService.java
new file mode 100644
index 0000000..da6eeb2
--- /dev/null
+++ b/service/java/com/android/server/appsearch/indexer/IndexerLocalService.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.indexer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CancellationSignal;
+import android.os.UserHandle;
+
+import com.android.server.LocalManagerRegistry;
+
+/**
+ * An interface for Indexers local services.
+ *
+ * @see LocalManagerRegistry#addManager
+ */
+public interface IndexerLocalService {
+    /** Runs a scheduled update for the user specified by userHandle. */
+    void doUpdateForUser(@NonNull UserHandle userHandle, @Nullable CancellationSignal signal);
+}
diff --git a/service/java/com/android/server/appsearch/indexer/IndexerMaintenanceConfig.java b/service/java/com/android/server/appsearch/indexer/IndexerMaintenanceConfig.java
new file mode 100644
index 0000000..8a71068
--- /dev/null
+++ b/service/java/com/android/server/appsearch/indexer/IndexerMaintenanceConfig.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.indexer;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import com.android.server.LocalManagerRegistry;
+import com.android.server.appsearch.appsindexer.AppsIndexerMaintenanceConfig;
+import com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceConfig;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Contains information needed to dispatch a maintenance job for an indexer. */
+public interface IndexerMaintenanceConfig {
+    int APPS_INDEXER = 0;
+    int CONTACTS_INDEXER = 1;
+
+    @IntDef(
+            value = {
+                APPS_INDEXER,
+                CONTACTS_INDEXER,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface IndexerType {}
+
+    /** Returns the {@link IndexerMaintenanceConfig} for the requested indexer type. */
+    @NonNull
+    static IndexerMaintenanceConfig getConfigForIndexer(@IndexerType int indexerType) {
+        if (indexerType == APPS_INDEXER) {
+            return AppsIndexerMaintenanceConfig.INSTANCE;
+        } else if (indexerType == CONTACTS_INDEXER) {
+            return ContactsIndexerMaintenanceConfig.INSTANCE;
+        } else {
+            throw new IllegalArgumentException(
+                    "Attempted to get config for invalid indexer type: " + indexerType);
+        }
+    }
+
+    /**
+     * Returns the local service for the indexer.
+     *
+     * @see LocalManagerRegistry#addManager
+     */
+    @NonNull
+    Class<? extends IndexerLocalService> getLocalService();
+
+    /** Returns the minimum job id for the indexer. */
+    int getMinJobId();
+}
diff --git a/service/java/com/android/server/appsearch/indexer/IndexerMaintenanceService.java b/service/java/com/android/server/appsearch/indexer/IndexerMaintenanceService.java
new file mode 100644
index 0000000..e68334a
--- /dev/null
+++ b/service/java/com/android/server/appsearch/indexer/IndexerMaintenanceService.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.indexer;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchEnvironmentFactory;
+import android.app.appsearch.annotation.CanIgnoreReturnValue;
+import android.app.appsearch.util.LogUtil;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService;
+import com.android.server.appsearch.indexer.IndexerMaintenanceConfig.IndexerType;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/** Dispatches maintenance tasks for various indexers. */
+public class IndexerMaintenanceService extends JobService {
+    private static final String TAG = "AppSearchIndexerMaintena";
+    private static final String EXTRA_USER_ID = "user_id";
+    private static final String INDEXER_TYPE = "indexer_type";
+
+    /**
+     * A mapping of userHandle-to-CancellationSignal. Since we schedule a separate job for each
+     * user, this JobService might be executing simultaneously for the various users, so we need to
+     * keep track of the cancellation signal for each user update so we stop the appropriate update
+     * when necessary.
+     */
+    @GuardedBy("mSignals")
+    private final Map<UserHandle, CancellationSignal> mSignals = new ArrayMap<>();
+
+    private final Executor mExecutor =
+            AppSearchEnvironmentFactory.getEnvironmentInstance()
+                    .createExecutorService(
+                            /* corePoolSize= */ 1,
+                            /* maximumPoolSize= */ 1,
+                            /* keepAliveTime= */ 60L,
+                            /* unit= */ TimeUnit.SECONDS,
+                            /* workQueue= */ new LinkedBlockingQueue<>(),
+                            /* priority= */ 0); // priority is unused.
+
+    /**
+     * Schedules an update job for the given device-user.
+     *
+     * @param userHandle Device user handle for whom the update job should be scheduled.
+     * @param periodic True to indicate that the job should be repeated.
+     * @param indexerType Indicates which {@link IndexerType} to schedule an update for.
+     * @param intervalMillis Millisecond interval for which this job should repeat.
+     */
+    public static void scheduleUpdateJob(
+            @NonNull Context context,
+            @NonNull UserHandle userHandle,
+            @IndexerType int indexerType,
+            boolean periodic,
+            long intervalMillis) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(userHandle);
+        int jobId = getJobIdForUser(userHandle, indexerType);
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        // For devices U and below, we have to schedule using ContactsIndexerMaintenanceService
+        // as it has the proper permissions in core/res/AndroidManifest.xml.
+        // IndexerMaintenanceService does not have the proper permissions on U. For simplicity, we
+        // can also use the same component for scheduling maintenance on U+.
+        ComponentName component =
+                new ComponentName(context, ContactsIndexerMaintenanceService.class);
+
+        final PersistableBundle extras = new PersistableBundle();
+        extras.putInt(EXTRA_USER_ID, userHandle.getIdentifier());
+        extras.putInt(INDEXER_TYPE, indexerType);
+        JobInfo.Builder jobInfoBuilder =
+                new JobInfo.Builder(jobId, component)
+                        .setExtras(extras)
+                        .setRequiresBatteryNotLow(true)
+                        .setRequiresDeviceIdle(true)
+                        .setPersisted(true);
+
+        if (periodic) {
+            // Specify a flex value of 1/2 the interval so that the job is scheduled to run
+            // in the [interval/2, interval) time window, assuming the other conditions are
+            // met. This avoids the scenario where the next update job is started within
+            // a short duration of the previous run.
+            jobInfoBuilder.setPeriodic(intervalMillis, /* flexMillis= */ intervalMillis / 2);
+        }
+        JobInfo jobInfo = jobInfoBuilder.build();
+        JobInfo pendingJobInfo = jobScheduler.getPendingJob(jobId);
+        // Don't reschedule a pending job if the parameters haven't changed.
+        if (jobInfo.equals(pendingJobInfo)) {
+            return;
+        }
+        jobScheduler.schedule(jobInfo);
+        if (LogUtil.DEBUG) {
+            Log.v(TAG, "Scheduled update job " + jobId + " for user " + userHandle);
+        }
+    }
+
+    /**
+     * Cancel update job for the given user.
+     *
+     * @param userHandle The user handle for whom the update job needs to be cancelled.
+     */
+    private static void cancelUpdateJob(
+            @NonNull Context context,
+            @NonNull UserHandle userHandle,
+            @IndexerType int indexerType) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(userHandle);
+        int jobId = getJobIdForUser(userHandle, indexerType);
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        jobScheduler.cancel(jobId);
+        if (LogUtil.DEBUG) {
+            Log.v(TAG, "Canceled update job " + jobId + " for user " + userHandle);
+        }
+    }
+
+    /**
+     * Check if a update job is scheduled for the given user.
+     *
+     * @param userHandle The user handle for whom the check for scheduled job needs to be performed
+     * @return true if a scheduled job exists
+     */
+    public static boolean isUpdateJobScheduled(
+            @NonNull Context context,
+            @NonNull UserHandle userHandle,
+            @IndexerType int indexerType) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(userHandle);
+        int jobId = getJobIdForUser(userHandle, indexerType);
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        return jobScheduler.getPendingJob(jobId) != null;
+    }
+
+    /**
+     * Cancel any scheduled update job for the given user. Checks if a update job for the given user
+     * exists before trying to cancel it.
+     *
+     * @param user The user for whom the update job needs to be cancelled.
+     */
+    public static void cancelUpdateJobIfScheduled(
+            @NonNull Context context, @NonNull UserHandle user, @IndexerType int indexerType) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(user);
+        try {
+            if (isUpdateJobScheduled(context, user, indexerType)) {
+                cancelUpdateJob(context, user, indexerType);
+            }
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Failed to cancel pending update job ", e);
+        }
+    }
+
+    /**
+     * Generate job ids in the range (MIN_INDEXER_JOB_ID, MAX_INDEXER_JOB_ID) to avoid conflicts
+     * with other jobs scheduled by the system service. The range corresponds to 21475 job ids,
+     * which is the maximum number of user ids in the system.
+     *
+     * @see com.android.server.pm.UserManagerService#MAX_USER_ID
+     */
+    private static int getJobIdForUser(
+            @NonNull UserHandle userHandle, @IndexerType int indexerType) {
+        Objects.requireNonNull(userHandle);
+        int baseJobId = IndexerMaintenanceConfig.getConfigForIndexer(indexerType).getMinJobId();
+        return baseJobId + userHandle.getIdentifier();
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        try {
+            int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ -1);
+            if (userId == -1) {
+                return false;
+            }
+
+            @IndexerType
+            int indexerType = params.getExtras().getInt(INDEXER_TYPE, /* defaultValue= */ -1);
+            if (indexerType == -1) {
+                return false;
+            }
+
+            if (LogUtil.DEBUG) {
+                Log.v(TAG, "Update job started for user " + userId);
+            }
+
+            UserHandle userHandle = UserHandle.getUserHandleForUid(userId);
+            final CancellationSignal oldSignal;
+            synchronized (mSignals) {
+                oldSignal = mSignals.get(userHandle);
+            }
+            if (oldSignal != null) {
+                // This could happen if we attempt to schedule a new job for the user while there's
+                // one already running.
+                Log.w(TAG, "Old update job still running for user " + userHandle);
+                oldSignal.cancel();
+            }
+            final CancellationSignal signal = new CancellationSignal();
+            synchronized (mSignals) {
+                mSignals.put(userHandle, signal);
+            }
+            mExecutor.execute(() -> doUpdateForUser(this, params, userHandle, signal));
+            return true;
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "IndexerMaintenanceService.onStartJob() failed ", e);
+            return false;
+        }
+    }
+
+    /**
+     * Triggers update from a background job for the given device-user using {@link
+     * ContactsIndexerManagerService.LocalService} manager.
+     *
+     * @param params Parameters from the job that triggered the update.
+     * @param userHandle Device user handle for whom the update job should be triggered.
+     * @param signal Used to indicate if the update task should be cancelled.
+     * @return A boolean representing whether the update operation completed or encountered an
+     *     issue. This return value is only used for testing purposes.
+     */
+    @VisibleForTesting
+    @CanIgnoreReturnValue
+    public boolean doUpdateForUser(
+            @NonNull Context context,
+            @Nullable JobParameters params,
+            @NonNull UserHandle userHandle,
+            @NonNull CancellationSignal signal) {
+        try {
+            Objects.requireNonNull(context);
+            Objects.requireNonNull(userHandle);
+            Objects.requireNonNull(signal);
+
+            @IndexerType int indexerType = params.getExtras().getInt(INDEXER_TYPE, -1);
+            if (indexerType == -1) {
+                return false;
+            }
+            Class<? extends IndexerLocalService> indexerLocalService =
+                    IndexerMaintenanceConfig.getConfigForIndexer(indexerType).getLocalService();
+            IndexerLocalService service = LocalManagerRegistry.getManager(indexerLocalService);
+            if (service == null) {
+                Log.e(
+                        TAG,
+                        "Background job failed to trigger Update because "
+                                + "Indexer.LocalService is not available.");
+                // If a background update job exists while an indexer is disabled, cancel the
+                // job after its first run. This will prevent any periodic jobs from being
+                // unnecessarily triggered repeatedly. If the service is null, it means the indexer
+                // is disabled. So the local service is not registered during the startup.
+                cancelUpdateJob(context, userHandle, indexerType);
+                return false;
+            }
+            service.doUpdateForUser(userHandle, signal);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Background job failed to trigger Update because ", e);
+            return false;
+        } finally {
+            jobFinished(params, signal.isCanceled());
+            synchronized (mSignals) {
+                if (signal == mSignals.get(userHandle)) {
+                    mSignals.remove(userHandle);
+                }
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        try {
+            final int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue */ -1);
+            if (userId == -1) {
+                return false;
+            }
+            UserHandle userHandle = UserHandle.getUserHandleForUid(userId);
+            // This will only run on S+ builds, so no need to do a version check.
+            if (LogUtil.DEBUG) {
+                Log.d(
+                        TAG,
+                        "Stopping update job for user "
+                                + userId
+                                + " because "
+                                + params.getStopReason());
+            }
+            synchronized (mSignals) {
+                final CancellationSignal signal = mSignals.get(userHandle);
+                if (signal != null) {
+                    signal.cancel();
+                    mSignals.remove(userHandle);
+                    // We had to stop the job early. Request reschedule.
+                    return true;
+                }
+            }
+            Log.e(TAG, "JobScheduler stopped an update that wasn't happening...");
+            return false;
+        } catch (RuntimeException e) {
+            Slog.wtf(TAG, "IndexerMaintenanceService.onStopJob() failed ", e);
+            return false;
+        }
+    }
+}
diff --git a/service/java/com/android/server/appsearch/observer/AppSearchObserverProxy.java b/service/java/com/android/server/appsearch/observer/AppSearchObserverProxy.java
index 8928516..1183732 100644
--- a/service/java/com/android/server/appsearch/observer/AppSearchObserverProxy.java
+++ b/service/java/com/android/server/appsearch/observer/AppSearchObserverProxy.java
@@ -22,7 +22,6 @@
 import android.app.appsearch.observer.DocumentChangeInfo;
 import android.app.appsearch.observer.ObserverCallback;
 import android.app.appsearch.observer.SchemaChangeInfo;
-import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -30,12 +29,12 @@
 import java.util.Objects;
 
 /**
- * A wrapper that adapts {@link android.app.appsearch.aidl.IAppSearchObserverProxy} to the
- * {@link android.app.appsearch.observer.ObserverCallback} interface.
+ * A wrapper that adapts {@link android.app.appsearch.aidl.IAppSearchObserverProxy} to the {@link
+ * android.app.appsearch.observer.ObserverCallback} interface.
  *
  * <p>When using this class, you must register for {@link android.os.IBinder#linkToDeath}
- * notifications on the stub you provide to the constructor, to unregister this class from
- * {@link com.android.server.appsearch.external.localstorage.AppSearchImpl} when binder dies.
+ * notifications on the stub you provide to the constructor, to unregister this class from {@link
+ * com.android.server.appsearch.external.localstorage.AppSearchImpl} when binder dies.
  *
  * @hide
  */
@@ -82,8 +81,12 @@
 
     @Override
     public boolean equals(@Nullable Object o) {
-        if (this == o) return true;
-        if (!(o instanceof AppSearchObserverProxy)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof AppSearchObserverProxy)) {
+            return false;
+        }
         AppSearchObserverProxy that = (AppSearchObserverProxy) o;
         return Objects.equals(mStub.asBinder(), that.mStub.asBinder());
     }
diff --git a/service/java/com/android/server/appsearch/stats/PlatformLogger.java b/service/java/com/android/server/appsearch/stats/PlatformLogger.java
index 35b4f19..9d1d65d 100644
--- a/service/java/com/android/server/appsearch/stats/PlatformLogger.java
+++ b/service/java/com/android/server/appsearch/stats/PlatformLogger.java
@@ -22,6 +22,7 @@
 import android.app.appsearch.exceptions.AppSearchException;
 import android.app.appsearch.stats.SchemaMigrationStats;
 import android.content.Context;
+import android.os.Build;
 import android.os.Process;
 import android.os.SystemClock;
 import android.util.ArrayMap;
@@ -31,12 +32,15 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.appsearch.InternalAppSearchLogger;
-import com.android.server.appsearch.FrameworkAppSearchConfig;
+import com.android.server.appsearch.ServiceAppSearchConfig;
 import com.android.server.appsearch.external.localstorage.stats.CallStats;
+import com.android.server.appsearch.external.localstorage.stats.ClickStats;
 import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats;
 import com.android.server.appsearch.external.localstorage.stats.RemoveStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchIntentStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchSessionStats;
 import com.android.server.appsearch.external.localstorage.stats.SearchStats;
 import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
 import com.android.server.appsearch.util.ApiCallRecord;
@@ -66,24 +70,23 @@
     private final Context mUserContext;
 
     // Manager holding the configuration flags
-    private final FrameworkAppSearchConfig mConfig;
+    private final ServiceAppSearchConfig mConfig;
 
     private final Random mRng = new Random();
     private final Object mLock = new Object();
 
     /**
-     * SparseArray to track how many stats we skipped due to
-     * {@link FrameworkAppSearchConfig#getCachedMinTimeIntervalBetweenSamplesMillis()}.
+     * SparseArray to track how many stats we skipped due to {@link
+     * ServiceAppSearchConfig#getCachedMinTimeIntervalBetweenSamplesMillis()}.
      *
-     * <p> We can have correct extrapolated number by adding those counts back when we log
-     * the same type of stats next time. E.g. the true count of an event could be estimated as:
+     * <p>We can have correct extrapolated number by adding those counts back when we log the same
+     * type of stats next time. E.g. the true count of an event could be estimated as:
      * SUM(sampling_interval * (num_skipped_sample + 1)) as est_count
      *
      * <p>The key to the SparseArray is {@link CallStats.CallType}
      */
     @GuardedBy("mLock")
-    private final SparseIntArray mSkippedSampleCountLocked =
-            new SparseIntArray();
+    private final SparseIntArray mSkippedSampleCountLocked = new SparseIntArray();
 
     /**
      * Map to cache the packageUid for each package.
@@ -93,26 +96,21 @@
      * <p>The entry will be removed whenever the app gets uninstalled
      */
     @GuardedBy("mLock")
-    private final Map<String, Integer> mPackageUidCacheLocked =
-            new ArrayMap<>();
+    private final Map<String, Integer> mPackageUidCacheLocked = new ArrayMap<>();
 
-    /**
-     * Elapsed time for last stats logged from boot in millis
-     */
+    /** Elapsed time for last stats logged from boot in millis */
     @GuardedBy("mLock")
     private long mLastPushTimeMillisLocked = 0;
 
     /**
-     * Record the last n API calls used by dumpsys to print debugging information about the
-     * sequence of the API calls, where n is specified by
-     * {@link FrameworkAppSearchConfig#getCachedApiCallStatsLimit()}.
+     * Record the last n API calls used by dumpsys to print debugging information about the sequence
+     * of the API calls, where n is specified by {@link
+     * ServiceAppSearchConfig#getCachedApiCallStatsLimit()}.
      */
     @GuardedBy("mLock")
     private ArrayDeque<ApiCallRecord> mLastNCalls = new ArrayDeque<>();
 
-    /**
-     * Helper class to hold platform specific stats for statsd.
-     */
+    /** Helper class to hold platform specific stats for statsd. */
     static final class ExtraStats {
         // UID for the calling package of the stats.
         final int mPackageUid;
@@ -128,12 +126,8 @@
         }
     }
 
-    /**
-     * Constructor
-     */
-    public PlatformLogger(
-            @NonNull Context userContext,
-            @NonNull FrameworkAppSearchConfig config) {
+    /** Constructor */
+    public PlatformLogger(@NonNull Context userContext, @NonNull ServiceAppSearchConfig config) {
         mUserContext = Objects.requireNonNull(userContext);
         mConfig = Objects.requireNonNull(config);
     }
@@ -228,6 +222,19 @@
     }
 
     @Override
+    public void logStats(@NonNull List<SearchSessionStats> searchSessionsStats) {
+        Objects.requireNonNull(searchSessionsStats);
+        if (searchSessionsStats.isEmpty()) {
+            return;
+        }
+
+        synchronized (mLock) {
+            // TODO(b/173532925): apply sampling if necessary
+            logStatsImplLocked(searchSessionsStats);
+        }
+    }
+
+    @Override
     public void removeCacheForPackage(@NonNull String packageName) {
         removeCachedUidForPackage(packageName);
     }
@@ -236,7 +243,7 @@
      * Removes cached UID for package.
      *
      * @return removed UID for the package, or {@code INVALID_UID} if package was not previously
-     * cached.
+     *     cached.
      */
     @CanIgnoreReturnValue
     @VisibleForTesting
@@ -249,9 +256,7 @@
         }
     }
 
-    /**
-     * Return a copy of the recorded {@link ApiCallRecord}.
-     */
+    /** Return a copy of the recorded {@link ApiCallRecord}. */
     @Override
     @NonNull
     public List<ApiCallRecord> getLastCalledApis() {
@@ -279,7 +284,8 @@
             final int numReportedCalls = 1;
 
             int hashCodeForDatabase = calculateHashCodeMd5(database);
-            AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_CALL_STATS_REPORTED,
+            AppSearchStatsLog.write(
+                    AppSearchStatsLog.APP_SEARCH_CALL_STATS_REPORTED,
                     extraStats.mSamplingInterval,
                     extraStats.mSkippedSampleCount,
                     extraStats.mPackageUid,
@@ -307,13 +313,14 @@
     @GuardedBy("mLock")
     private void logStatsImplLocked(@NonNull SetSchemaStats stats) {
         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
-        ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(),
-                CallStats.CALL_TYPE_SET_SCHEMA);
+        ExtraStats extraStats =
+                createExtraStatsLocked(stats.getPackageName(), CallStats.CALL_TYPE_SET_SCHEMA);
         String database = stats.getDatabase();
         try {
             int hashCodeForDatabase = calculateHashCodeMd5(database);
             // ignore close exception
-            AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_SET_SCHEMA_STATS_REPORTED,
+            AppSearchStatsLog.write(
+                    AppSearchStatsLog.APP_SEARCH_SET_SCHEMA_STATS_REPORTED,
                     extraStats.mSamplingInterval,
                     extraStats.mSkippedSampleCount,
                     extraStats.mPackageUid,
@@ -355,13 +362,15 @@
     @GuardedBy("mLock")
     private void logStatsImplLocked(@NonNull SchemaMigrationStats stats) {
         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
-        ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(),
-                CallStats.CALL_TYPE_SCHEMA_MIGRATION);
+        ExtraStats extraStats =
+                createExtraStatsLocked(
+                        stats.getPackageName(), CallStats.CALL_TYPE_SCHEMA_MIGRATION);
         String database = stats.getDatabase();
         try {
             int hashCodeForDatabase = calculateHashCodeMd5(database);
             // ignore close exception
-            AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_SET_SCHEMA_STATS_REPORTED,
+            AppSearchStatsLog.write(
+                    AppSearchStatsLog.APP_SEARCH_SET_SCHEMA_STATS_REPORTED,
                     extraStats.mSamplingInterval,
                     extraStats.mSkippedSampleCount,
                     extraStats.mPackageUid,
@@ -391,12 +400,13 @@
     @GuardedBy("mLock")
     private void logStatsImplLocked(@NonNull PutDocumentStats stats) {
         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
-        ExtraStats extraStats = createExtraStatsLocked(
-                stats.getPackageName(), CallStats.CALL_TYPE_PUT_DOCUMENT);
+        ExtraStats extraStats =
+                createExtraStatsLocked(stats.getPackageName(), CallStats.CALL_TYPE_PUT_DOCUMENT);
         String database = stats.getDatabase();
         try {
             int hashCodeForDatabase = calculateHashCodeMd5(database);
-            AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_PUT_DOCUMENT_STATS_REPORTED,
+            AppSearchStatsLog.write(
+                    AppSearchStatsLog.APP_SEARCH_PUT_DOCUMENT_STATS_REPORTED,
                     extraStats.mSamplingInterval,
                     extraStats.mSkippedSampleCount,
                     extraStats.mPackageUid,
@@ -411,7 +421,7 @@
                     stats.getNativeIndexMergeLatencyMillis(),
                     stats.getNativeDocumentSizeBytes(),
                     stats.getNativeNumTokensIndexed(),
-                    /*nativeExceededMaxNumTokens=*/false /* Deprecated and removed */);
+                    /* nativeExceededMaxNumTokens= */ false /* Deprecated and removed */);
         } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
             // TODO(b/184204720) report hashing error to statsd
             //  We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database,
@@ -428,13 +438,14 @@
     @GuardedBy("mLock")
     private void logStatsImplLocked(@NonNull SearchStats stats) {
         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
-        ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(),
-                CallStats.CALL_TYPE_SEARCH);
+        ExtraStats extraStats =
+                createExtraStatsLocked(stats.getPackageName(), CallStats.CALL_TYPE_SEARCH);
         String database = stats.getDatabase();
         try {
             int hashCodeForDatabase = calculateHashCodeMd5(database);
             int hashCodeForSearchSourceLogTag = calculateHashCodeMd5(stats.getSearchSourceLogTag());
-            AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_QUERY_STATS_REPORTED,
+            AppSearchStatsLog.write(
+                    AppSearchStatsLog.APP_SEARCH_QUERY_STATS_REPORTED,
                     extraStats.mSamplingInterval,
                     extraStats.mSkippedSampleCount,
                     extraStats.mPackageUid,
@@ -484,9 +495,10 @@
     @GuardedBy("mLock")
     private void logStatsImplLocked(@NonNull InitializeStats stats) {
         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
-        ExtraStats extraStats = createExtraStatsLocked(/*packageName=*/ null,
-                CallStats.CALL_TYPE_INITIALIZE);
-        AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_INITIALIZE_STATS_REPORTED,
+        ExtraStats extraStats =
+                createExtraStatsLocked(/* packageName= */ null, CallStats.CALL_TYPE_INITIALIZE);
+        AppSearchStatsLog.write(
+                AppSearchStatsLog.APP_SEARCH_INITIALIZE_STATS_REPORTED,
                 extraStats.mSamplingInterval,
                 extraStats.mSkippedSampleCount,
                 extraStats.mPackageUid,
@@ -512,9 +524,10 @@
     @GuardedBy("mLock")
     private void logStatsImplLocked(@NonNull OptimizeStats stats) {
         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
-        ExtraStats extraStats = createExtraStatsLocked(/*packageName=*/ null,
-                CallStats.CALL_TYPE_OPTIMIZE);
-        AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_OPTIMIZE_STATS_REPORTED,
+        ExtraStats extraStats =
+                createExtraStatsLocked(/* packageName= */ null, CallStats.CALL_TYPE_OPTIMIZE);
+        AppSearchStatsLog.write(
+                AppSearchStatsLog.APP_SEARCH_OPTIMIZE_STATS_REPORTED,
                 extraStats.mSamplingInterval,
                 extraStats.mSkippedSampleCount,
                 stats.getStatusCode(),
@@ -530,9 +543,106 @@
                 stats.getTimeSinceLastOptimizeMillis());
     }
 
+    @GuardedBy("mLock")
+    private void logStatsImplLocked(@NonNull List<SearchSessionStats> searchSessionsStats) {
+        for (int i = 0; i < searchSessionsStats.size(); ++i) {
+            SearchSessionStats searchSessionStats = searchSessionsStats.get(i);
+            logStatsImplLocked(searchSessionStats);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void logStatsImplLocked(@NonNull SearchSessionStats searchSessionStats) {
+        List<SearchIntentStats> searchIntentsStats = searchSessionStats.getSearchIntentsStats();
+        for (int i = 0; i < searchIntentsStats.size(); ++i) {
+            SearchIntentStats searchIntentStats = searchIntentsStats.get(i);
+            logStatsImplLocked(searchIntentStats);
+        }
+
+        // Additionally log the end session search intent stats.
+        SearchIntentStats endSessionSearchIntentStats =
+                searchSessionStats.getEndSessionSearchIntentStats();
+        if (endSessionSearchIntentStats != null) {
+            logStatsImplLocked(endSessionSearchIntentStats);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void logStatsImplLocked(@NonNull SearchIntentStats searchIntentStats) {
+        int packageUid = getPackageUidAsUserLocked(searchIntentStats.getPackageName());
+        String database = searchIntentStats.getDatabase();
+
+        // Prepare click related objects for atoms.
+        List<ClickStats> clicksStats = searchIntentStats.getClicksStats();
+        long[] clicksTimestampMillis = new long[clicksStats.size()];
+        long[] clicksTimeStayOnResultMillis = new long[clicksStats.size()];
+        int[] clicksResultRankInBlock = new int[clicksStats.size()];
+        int[] clicksResultRankGlobal = new int[clicksStats.size()];
+        int numClicks = clicksStats.size();
+        int numGoodClicks = 0;
+        for (int i = 0; i < clicksStats.size(); ++i) {
+            ClickStats clickStats = clicksStats.get(i);
+
+            clicksTimestampMillis[i] = clickStats.getTimestampMillis();
+            clicksTimeStayOnResultMillis[i] = clickStats.getTimeStayOnResultMillis();
+            clicksResultRankInBlock[i] = clickStats.getResultRankInBlock();
+            clicksResultRankGlobal[i] = clickStats.getResultRankGlobal();
+            if (clickStats.isGoodClick()) {
+                ++numGoodClicks;
+            }
+        }
+
+        int hashCodeForDatabase;
+        try {
+            hashCodeForDatabase = calculateHashCodeMd5(database);
+        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+            // Something is wrong while calculating the hash code for database. Assign the hash
+            // value with 0xFFFFFFFF, and log the error message.
+            // This shouldn't happen since we always use "MD5" and "UTF-8".
+            hashCodeForDatabase = 0xFFFFFFFF;
+            if (database != null) {
+                Log.e(TAG, "Error calculating hash code for database " + database, e);
+            }
+        }
+
+        // Write atoms.
+        AppSearchStatsLog.write(
+                AppSearchStatsLog.APP_SEARCH_USAGE_SEARCH_INTENT_STATS_REPORTED,
+                packageUid,
+                hashCodeForDatabase,
+                searchIntentStats.getTimestampMillis(),
+                searchIntentStats.getNumResultsFetched(),
+                searchIntentStats.getQueryCorrectionType(),
+                clicksTimestampMillis,
+                clicksTimeStayOnResultMillis,
+                clicksResultRankInBlock,
+                clicksResultRankGlobal);
+
+        // Only log restricted atoms for QUERY_CORRECTION_TYPE_ABANDONMENT to catch query correction
+        // for common synonyms, abbreviation, nicknames and rebranded names, e.g. "Robert" -> "Bob".
+        boolean logRestrictedAtom =
+                searchIntentStats.getQueryCorrectionType()
+                        == SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+        // Restricted atoms are only available on U+.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && logRestrictedAtom) {
+            String prevQuery = searchIntentStats.getPrevQuery();
+            String currQuery = searchIntentStats.getCurrQuery();
+            AppSearchStatsLog.write(
+                    AppSearchStatsLog.APP_SEARCH_USAGE_SEARCH_INTENT_RAW_QUERY_STATS_REPORTED,
+                    searchIntentStats.getPackageName(),
+                    hashCodeForDatabase,
+                    prevQuery == null ? "" : prevQuery,
+                    currQuery == null ? "" : currQuery,
+                    searchIntentStats.getNumResultsFetched(),
+                    numClicks,
+                    numGoodClicks,
+                    searchIntentStats.getQueryCorrectionType());
+        }
+    }
+
     /**
      * This method will drop the earliest stats in the queue when the number of calls is at the
-     * capacity specified by {@link FrameworkAppSearchConfig#getCachedApiCallStatsLimit()}.
+     * capacity specified by {@link ServiceAppSearchConfig#getCachedApiCallStatsLimit()}.
      */
     @GuardedBy("mLock")
     private void trimExcessStatsQueueLocked() {
@@ -549,8 +659,8 @@
     /**
      * Record {@link ApiCallRecord} to {@link #mLastNCalls} for dumpsys.
      *
-     * <p> This method will automatically drop the earliest stats when the number of calls is at the
-     * capacity specified by {@link FrameworkAppSearchConfig#getCachedApiCallStatsLimit()}.
+     * <p>This method will automatically drop the earliest stats when the number of calls is at the
+     * capacity specified by {@link ServiceAppSearchConfig#getCachedApiCallStatsLimit()}.
      */
     @GuardedBy("mLock")
     @VisibleForTesting
@@ -568,14 +678,14 @@
      */
     @VisibleForTesting
     @NonNull
-    static int calculateHashCodeMd5(@Nullable String str) throws
-            NoSuchAlgorithmException, UnsupportedEncodingException {
+    static int calculateHashCodeMd5(@Nullable String str)
+            throws NoSuchAlgorithmException, UnsupportedEncodingException {
         if (str == null) {
             return -1;
         }
 
         MessageDigest md = MessageDigest.getInstance("MD5");
-        md.update(str.getBytes(/*charsetName=*/ "UTF-8"));
+        md.update(str.getBytes(/* charsetName= */ "UTF-8"));
         byte[] digest = md.digest();
 
         // Since MD5 generates 16 bytes digest, we don't need to check the length here to see
@@ -593,8 +703,7 @@
     /**
      * Creates {@link ExtraStats} to hold additional information generated for logging.
      *
-     * <p>This method is called by most of logStatsImplLocked functions to reduce code
-     * duplication.
+     * <p>This method is called by most of logStatsImplLocked functions to reduce code duplication.
      */
     // TODO(b/173532925) Once we add CTS test for logging atoms and can inspect the result, we can
     // remove this @VisibleForTesting and directly use PlatformLogger.logStats to test sampling and
@@ -602,8 +711,8 @@
     @VisibleForTesting
     @GuardedBy("mLock")
     @NonNull
-    ExtraStats createExtraStatsLocked(@Nullable String packageName,
-            @CallStats.CallType int callType) {
+    ExtraStats createExtraStatsLocked(
+            @Nullable String packageName, @CallStats.CallType int callType) {
         int packageUid = Process.INVALID_UID;
         if (packageName != null) {
             packageUid = getPackageUidAsUserLocked(packageName);
@@ -615,8 +724,8 @@
         // Or we can retrieve samplingRatio at beginning and pass along
         // as function parameter, but it will make code less cleaner with some duplication.
         int samplingInterval = getSamplingIntervalFromConfig(callType);
-        int skippedSampleCount = mSkippedSampleCountLocked.get(callType,
-                /*valueOfKeyIfNotFound=*/ 0);
+        int skippedSampleCount =
+                mSkippedSampleCountLocked.get(callType, /* valueOfKeyIfNotFound= */ 0);
         mSkippedSampleCountLocked.put(callType, 0);
 
         return new ExtraStats(packageUid, samplingInterval, skippedSampleCount);
@@ -645,7 +754,7 @@
         long currentTimeMillis = SystemClock.elapsedRealtime();
         if (mLastPushTimeMillisLocked
                 > currentTimeMillis - mConfig.getCachedMinTimeIntervalBetweenSamplesMillis()) {
-            int count = mSkippedSampleCountLocked.get(callType, /*valueOfKeyIfNotFound=*/ 0);
+            int count = mSkippedSampleCountLocked.get(callType, /* valueOfKeyIfNotFound= */ 0);
             ++count;
             mSkippedSampleCountLocked.put(callType, count);
             return false;
@@ -684,7 +793,7 @@
         return packageUid;
     }
 
-    /** Returns sampling ratio for stats type specified form {@link FrameworkAppSearchConfig}. */
+    /** Returns sampling ratio for stats type specified form {@link ServiceAppSearchConfig}. */
     private int getSamplingIntervalFromConfig(@CallStats.CallType int statsType) {
         switch (statsType) {
             case CallStats.CALL_TYPE_PUT_DOCUMENTS:
diff --git a/service/java/com/android/server/appsearch/stats/StatsCollector.java b/service/java/com/android/server/appsearch/stats/StatsCollector.java
index 45c7f9e..84305ac 100644
--- a/service/java/com/android/server/appsearch/stats/StatsCollector.java
+++ b/service/java/com/android/server/appsearch/stats/StatsCollector.java
@@ -21,6 +21,7 @@
 import android.annotation.UserIdInt;
 import android.app.StatsManager;
 import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.util.ExceptionUtil;
 import android.app.appsearch.util.LogUtil;
 import android.content.Context;
 import android.os.UserHandle;
@@ -29,7 +30,6 @@
 
 import com.android.server.appsearch.AppSearchUserInstance;
 import com.android.server.appsearch.AppSearchUserInstanceManager;
-import com.android.server.appsearch.util.ExceptionUtil;
 
 import com.google.android.icing.proto.DocumentStorageInfoProto;
 import com.google.android.icing.proto.IndexStorageInfoProto;
@@ -61,8 +61,7 @@
      * existing instance will be returned.
      */
     @NonNull
-    public static StatsCollector getInstance(@NonNull Context context,
-            @NonNull Executor executor) {
+    public static StatsCollector getInstance(@NonNull Context context, @NonNull Executor executor) {
         Objects.requireNonNull(context);
         Objects.requireNonNull(executor);
         if (sStatsCollector == null) {
@@ -78,7 +77,7 @@
     private StatsCollector(@NonNull Context context, @NonNull Executor executor) {
         mStatsManager = context.getSystemService(StatsManager.class);
         if (mStatsManager != null) {
-            registerAtom(AppSearchStatsLog.APP_SEARCH_STORAGE_INFO, /*policy=*/ null, executor);
+            registerAtom(AppSearchStatsLog.APP_SEARCH_STORAGE_INFO, /* policy= */ null, executor);
             if (LogUtil.DEBUG) {
                 Log.d(TAG, "atoms registered");
             }
@@ -91,8 +90,8 @@
      * {@inheritDoc}
      *
      * @return {@link StatsManager#PULL_SUCCESS} with list of atoms (potentially empty) if pull
-     * succeeded, {@link StatsManager#PULL_SKIP} if pull was too frequent or atom ID is
-     * unexpected.
+     *     succeeded, {@link StatsManager#PULL_SKIP} if pull was too frequent or atom ID is
+     *     unexpected.
      */
     @Override
     public int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) {
@@ -113,15 +112,13 @@
         for (int i = 0; i < userHandles.size(); i++) {
             UserHandle userHandle = userHandles.get(i);
             try {
-                AppSearchUserInstance userInstance = userInstanceManager.getUserInstance(
-                        userHandle);
+                AppSearchUserInstance userInstance =
+                        userInstanceManager.getUserInstance(userHandle);
                 StorageInfoProto storageInfoProto =
                         userInstance.getAppSearchImpl().getRawStorageInfoProto();
                 data.add(buildStatsEvent(userHandle.getIdentifier(), storageInfoProto));
             } catch (AppSearchException | RuntimeException e) {
-                Log.e(TAG,
-                        "Failed to pull the storage info for user " + userHandle.toString(),
-                        e);
+                Log.e(TAG, "Failed to pull the storage info for user " + userHandle.toString(), e);
                 ExceptionUtil.handleException(e);
             }
         }
@@ -137,18 +134,20 @@
     /**
      * Registers and configures the callback for the pulled atom.
      *
-     * @param atomId   The id of the atom
-     * @param policy   Optional metadata specifying the timeout, cool down time etc. statsD would
-     *                 use default values if it is null
+     * @param atomId The id of the atom
+     * @param policy Optional metadata specifying the timeout, cool down time etc. statsD would use
+     *     default values if it is null
      * @param executor The executor in which to run the callback
      */
-    private void registerAtom(int atomId, @Nullable StatsManager.PullAtomMetadata policy,
+    private void registerAtom(
+            int atomId,
+            @Nullable StatsManager.PullAtomMetadata policy,
             @NonNull Executor executor) {
-        mStatsManager.setPullAtomCallback(atomId, policy, executor, /*callback=*/this);
+        mStatsManager.setPullAtomCallback(atomId, policy, executor, /* callback= */ this);
     }
 
-    private static StatsEvent buildStatsEvent(@UserIdInt int userId,
-            @NonNull StorageInfoProto storageInfoProto) {
+    private static StatsEvent buildStatsEvent(
+            @UserIdInt int userId, @NonNull StorageInfoProto storageInfoProto) {
         return AppSearchStatsLog.buildStatsEvent(
                 AppSearchStatsLog.APP_SEARCH_STORAGE_INFO,
                 userId,
@@ -158,8 +157,7 @@
                 getIndexStorageInfoBytes(storageInfoProto.getIndexStorageInfo()));
     }
 
-    private static byte[] getDocumentStorageInfoBytes(
-            @NonNull DocumentStorageInfoProto proto) {
+    private static byte[] getDocumentStorageInfoBytes(@NonNull DocumentStorageInfoProto proto) {
         // Make sure we only log the fields defined in the atom in case new fields are added in
         // IcingLib
         DocumentStorageInfoProto.Builder builder = DocumentStorageInfoProto.newBuilder();
@@ -191,8 +189,7 @@
         return builder.build().toByteArray();
     }
 
-    private static byte[] getIndexStorageInfoBytes(
-            @NonNull IndexStorageInfoProto proto) {
+    private static byte[] getIndexStorageInfoBytes(@NonNull IndexStorageInfoProto proto) {
         // Make sure we only log the fields defined in the atom in case new fields are added in
         // IcingLib
         IndexStorageInfoProto.Builder builder = IndexStorageInfoProto.newBuilder();
diff --git a/service/java/com/android/server/appsearch/transformer/EnterpriseSearchResultPageTransformer.java b/service/java/com/android/server/appsearch/transformer/EnterpriseSearchResultPageTransformer.java
index 68d157d..92c64a6 100644
--- a/service/java/com/android/server/appsearch/transformer/EnterpriseSearchResultPageTransformer.java
+++ b/service/java/com/android/server/appsearch/transformer/EnterpriseSearchResultPageTransformer.java
@@ -27,13 +27,10 @@
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Transforms the retrieved documents in {@link SearchResult} for enterprise access.
- */
+/** Transforms the retrieved documents in {@link SearchResult} for enterprise access. */
 public final class EnterpriseSearchResultPageTransformer {
 
-    private EnterpriseSearchResultPageTransformer() {
-    }
+    private EnterpriseSearchResultPageTransformer() {}
 
     /**
      * Transforms a {@link SearchResultPage}, applying enterprise document transformations in the
@@ -62,10 +59,13 @@
     @NonNull
     static SearchResult transformSearchResult(@NonNull SearchResult originalResult) {
         Objects.requireNonNull(originalResult);
-        boolean shouldTransformDocument = shouldTransformDocument(originalResult.getPackageName(),
-                originalResult.getDatabaseName(), originalResult.getGenericDocument());
-        boolean shouldTransformJoinedResults = shouldTransformSearchResults(
-                originalResult.getJoinedResults());
+        boolean shouldTransformDocument =
+                shouldTransformDocument(
+                        originalResult.getPackageName(),
+                        originalResult.getDatabaseName(),
+                        originalResult.getGenericDocument());
+        boolean shouldTransformJoinedResults =
+                shouldTransformSearchResults(originalResult.getJoinedResults());
         // Split the transform check so we can avoid transforming both the original and joined
         // results when only one actually needs to be transformed.
         if (!shouldTransformDocument && !shouldTransformJoinedResults) {
@@ -73,8 +73,11 @@
         }
         SearchResult.Builder builder = new SearchResult.Builder(originalResult);
         if (shouldTransformDocument) {
-            GenericDocument transformedDocument = transformDocument(originalResult.getPackageName(),
-                    originalResult.getDatabaseName(), originalResult.getGenericDocument());
+            GenericDocument transformedDocument =
+                    transformDocument(
+                            originalResult.getPackageName(),
+                            originalResult.getDatabaseName(),
+                            originalResult.getGenericDocument());
             builder.setGenericDocument(transformedDocument);
         }
         if (shouldTransformJoinedResults) {
@@ -93,10 +96,12 @@
      * the original document if the combination is not recognized.
      */
     @NonNull
-    public static GenericDocument transformDocument(@NonNull String packageName,
-            @NonNull String databaseName, @NonNull GenericDocument originalDocument) {
-        if (PersonEnterpriseTransformer.shouldTransform(packageName, databaseName,
-                originalDocument.getSchemaType())) {
+    public static GenericDocument transformDocument(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull GenericDocument originalDocument) {
+        if (PersonEnterpriseTransformer.shouldTransform(
+                packageName, databaseName, originalDocument.getSchemaType())) {
             return PersonEnterpriseTransformer.transformDocument(originalDocument);
         }
         return originalDocument;
@@ -116,8 +121,10 @@
 
     /** Checks if we need to transform the {@link SearchResult}. */
     private static boolean shouldTransformSearchResult(@NonNull SearchResult searchResult) {
-        return shouldTransformDocument(searchResult.getPackageName(),
-                searchResult.getDatabaseName(), searchResult.getGenericDocument())
+        return shouldTransformDocument(
+                        searchResult.getPackageName(),
+                        searchResult.getDatabaseName(),
+                        searchResult.getGenericDocument())
                 || shouldTransformSearchResults(searchResult.getJoinedResults());
     }
 
@@ -131,11 +138,12 @@
         return false;
     }
 
-
     /** Checks if we need to transform the {@link GenericDocument}. */
-    private static boolean shouldTransformDocument(@NonNull String packageName,
-            @NonNull String databaseName, @NonNull GenericDocument document) {
-        return PersonEnterpriseTransformer.shouldTransform(packageName, databaseName,
-                document.getSchemaType());
+    private static boolean shouldTransformDocument(
+            @NonNull String packageName,
+            @NonNull String databaseName,
+            @NonNull GenericDocument document) {
+        return PersonEnterpriseTransformer.shouldTransform(
+                packageName, databaseName, document.getSchemaType());
     }
 }
diff --git a/service/java/com/android/server/appsearch/transformer/EnterpriseSearchSpecTransformer.java b/service/java/com/android/server/appsearch/transformer/EnterpriseSearchSpecTransformer.java
index f333800..60ae2e6 100644
--- a/service/java/com/android/server/appsearch/transformer/EnterpriseSearchSpecTransformer.java
+++ b/service/java/com/android/server/appsearch/transformer/EnterpriseSearchSpecTransformer.java
@@ -35,21 +35,20 @@
 //  currently it applies to all Person types
 public final class EnterpriseSearchSpecTransformer {
 
-    private EnterpriseSearchSpecTransformer() {
-    }
+    private EnterpriseSearchSpecTransformer() {}
 
     /**
      * Transforms a {@link SearchSpec}, adding property filters and projections that restrict the
      * allowed properties for certain schema types when accessed through an enterprise session.
-     * <p>
-     * Currently, we only add filters and projections for {@link Person} schema type.
+     *
+     * <p>Currently, we only add filters and projections for {@link Person} schema type.
      */
     @NonNull
     public static SearchSpec transformSearchSpec(@NonNull SearchSpec searchSpec) {
         Objects.requireNonNull(searchSpec);
         boolean shouldTransformSearchSpecFilters = shouldTransformSearchSpecFilters(searchSpec);
-        boolean shouldTransformJoinSpecFilters = shouldTransformJoinSpecFilters(
-                searchSpec.getJoinSpec());
+        boolean shouldTransformJoinSpecFilters =
+                shouldTransformJoinSpecFilters(searchSpec.getJoinSpec());
         if (!shouldTransformSearchSpecFilters && !shouldTransformJoinSpecFilters) {
             return searchSpec;
         }
@@ -60,8 +59,8 @@
         if (shouldTransformJoinSpecFilters) {
             JoinSpec joinSpec = searchSpec.getJoinSpec();
             JoinSpec.Builder joinSpecBuilder = new JoinSpec.Builder(joinSpec);
-            joinSpecBuilder.setNestedSearch(joinSpec.getNestedQuery(),
-                    transformSearchSpec(joinSpec.getNestedSearchSpec()));
+            joinSpecBuilder.setNestedSearch(
+                    joinSpec.getNestedQuery(), transformSearchSpec(joinSpec.getNestedSearchSpec()));
             builder.setJoinSpec(joinSpecBuilder.build());
         }
         return builder.build();
diff --git a/service/java/com/android/server/appsearch/transformer/PersonEnterpriseTransformer.java b/service/java/com/android/server/appsearch/transformer/PersonEnterpriseTransformer.java
index 052d70b..aeaa7d7 100644
--- a/service/java/com/android/server/appsearch/transformer/PersonEnterpriseTransformer.java
+++ b/service/java/com/android/server/appsearch/transformer/PersonEnterpriseTransformer.java
@@ -40,73 +40,75 @@
 import java.util.Objects;
 import java.util.Set;
 
-/**
- * Contains various transforms for {@link Person} enterprise access.
- */
+/** Contains various transforms for {@link Person} enterprise access. */
 final class PersonEnterpriseTransformer {
     private static final String TAG = "AppSearchPersonEnterpri";
 
     // These constants are hidden in ContactsContract.Contacts
-    private static final Uri CORP_CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI,
-            "contacts_corp");
+    private static final Uri CORP_CONTENT_URI =
+            Uri.withAppendedPath(AUTHORITY_URI, "contacts_corp");
     private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000;
     private static final String ENTERPRISE_CONTACT_LOOKUP_PREFIX = "c-";
 
     // Person externalUri should begin with "content://com.android.contacts/contacts/lookup"
     private static final String CONTACTS_LOOKUP_URI_PREFIX = Contacts.CONTENT_LOOKUP_URI.toString();
 
-    private static final List<String> PERSON_ACCESSIBLE_PROPERTIES = List.of(
-            Person.PERSON_PROPERTY_NAME,
-            Person.PERSON_PROPERTY_GIVEN_NAME,
-            Person.PERSON_PROPERTY_MIDDLE_NAME,
-            Person.PERSON_PROPERTY_FAMILY_NAME,
-            Person.PERSON_PROPERTY_EXTERNAL_URI,
-            Person.PERSON_PROPERTY_ADDITIONAL_NAME_TYPES,
-            Person.PERSON_PROPERTY_ADDITIONAL_NAMES,
-            Person.PERSON_PROPERTY_IMAGE_URI,
-            Person.PERSON_PROPERTY_CONTACT_POINTS + "."
-                    + ContactPoint.CONTACT_POINT_PROPERTY_LABEL,
-            Person.PERSON_PROPERTY_CONTACT_POINTS + "."
-                    + ContactPoint.CONTACT_POINT_PROPERTY_EMAIL,
-            Person.PERSON_PROPERTY_CONTACT_POINTS + "."
-                    + ContactPoint.CONTACT_POINT_PROPERTY_TELEPHONE);
+    private static final List<String> PERSON_ACCESSIBLE_PROPERTIES =
+            List.of(
+                    Person.PERSON_PROPERTY_NAME,
+                    Person.PERSON_PROPERTY_GIVEN_NAME,
+                    Person.PERSON_PROPERTY_MIDDLE_NAME,
+                    Person.PERSON_PROPERTY_FAMILY_NAME,
+                    Person.PERSON_PROPERTY_EXTERNAL_URI,
+                    Person.PERSON_PROPERTY_ADDITIONAL_NAME_TYPES,
+                    Person.PERSON_PROPERTY_ADDITIONAL_NAMES,
+                    Person.PERSON_PROPERTY_IMAGE_URI,
+                    Person.PERSON_PROPERTY_CONTACT_POINTS
+                            + "."
+                            + ContactPoint.CONTACT_POINT_PROPERTY_LABEL,
+                    Person.PERSON_PROPERTY_CONTACT_POINTS
+                            + "."
+                            + ContactPoint.CONTACT_POINT_PROPERTY_EMAIL,
+                    Person.PERSON_PROPERTY_CONTACT_POINTS
+                            + "."
+                            + ContactPoint.CONTACT_POINT_PROPERTY_TELEPHONE);
 
     @VisibleForTesting
-    static final Set<String> PERSON_ACCESSIBLE_PROPERTIES_SET = new ArraySet<>(
-            PERSON_ACCESSIBLE_PROPERTIES);
+    static final Set<String> PERSON_ACCESSIBLE_PROPERTIES_SET =
+            new ArraySet<>(PERSON_ACCESSIBLE_PROPERTIES);
 
-    private PersonEnterpriseTransformer() {
-    }
+    private PersonEnterpriseTransformer() {}
 
     /**
      * Returns whether or not a document of the given package, database, and schema type combination
      * should be transformed for enterprise.
      */
-    static boolean shouldTransform(@NonNull String packageName, @NonNull String databaseName,
-            @NonNull String schemaType) {
-        return schemaType.equals(Person.SCHEMA_TYPE) && packageName.equals("android")
+    static boolean shouldTransform(
+            @NonNull String packageName, @NonNull String databaseName, @NonNull String schemaType) {
+        return schemaType.equals(Person.SCHEMA_TYPE)
+                && packageName.equals("android")
                 && databaseName.equals(AppSearchHelper.DATABASE_NAME);
     }
 
     /**
      * Transforms the imageUri and externalUri properties of a Person document to their enterprise
      * versions which are the corp thumbnail uri and corp lookup uri respectively.
-     * <p>
-     * When contacts are accessed through CP2's enterprise uri, CP2 replaces the contact id with an
-     * enterprise contact id (the original contact id plus a base enterprise id
-     * {@link ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE}). The corp thumbnail uri keeps
-     * the original contact id, but the corp lookup uri uses the enterprise contact id.
-     * <p>
-     * In this method, we only transform the imageUri and externalUri properties, and we leave the
-     * document id untouched, since changing the document id would interfere with retrieving
+     *
+     * <p>When contacts are accessed through CP2's enterprise uri, CP2 replaces the contact id with
+     * an enterprise contact id (the original contact id plus a base enterprise id {@link
+     * ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE}). The corp thumbnail uri keeps the
+     * original contact id, but the corp lookup uri uses the enterprise contact id.
+     *
+     * <p>In this method, we only transform the imageUri and externalUri properties, and we leave
+     * the document id untouched, since changing the document id would interfere with retrieving
      * documents by id.
      */
     @NonNull
     static GenericDocument transformDocument(@NonNull GenericDocument originalDocument) {
         Objects.requireNonNull(originalDocument);
         String imageUri = originalDocument.getPropertyString(Person.PERSON_PROPERTY_IMAGE_URI);
-        String externalUri = originalDocument.getPropertyString(
-                Person.PERSON_PROPERTY_EXTERNAL_URI);
+        String externalUri =
+                originalDocument.getPropertyString(Person.PERSON_PROPERTY_EXTERNAL_URI);
         // Only transform the properties if they're present in the document. If neither property is
         // present, just return the original document
         if (imageUri == null && externalUri == null) {
@@ -117,15 +119,15 @@
         if (imageUri != null) {
             try {
                 long contactId = Long.parseLong(originalDocument.getId());
-                transformedDocumentBuilder.setPropertyString(Person.PERSON_PROPERTY_IMAGE_URI,
-                        getCorpImageUri(contactId));
+                transformedDocumentBuilder.setPropertyString(
+                        Person.PERSON_PROPERTY_IMAGE_URI, getCorpImageUri(contactId));
             } catch (NumberFormatException e) {
                 Log.w(TAG, "Failed to set imageUri property", e);
             }
         }
         if (externalUri != null) {
-            transformedDocumentBuilder.setPropertyString(Person.PERSON_PROPERTY_EXTERNAL_URI,
-                    getCorpLookupUri(externalUri));
+            transformedDocumentBuilder.setPropertyString(
+                    Person.PERSON_PROPERTY_EXTERNAL_URI, getCorpLookupUri(externalUri));
         }
         return transformedDocumentBuilder.build();
     }
@@ -139,8 +141,9 @@
     @NonNull
     static String getCorpImageUri(long contactId) {
         // https://cs.android.com/android/platform/superproject/main/+/main:packages/providers/ContactsProvider/src/com/android/providers/contacts/enterprise/EnterpriseContactsCursorWrapper.java;l=178;drc=a9d2c06a03a653954629ff10070ebbe4ea87d526
-        return ContentUris.appendId(CORP_CONTENT_URI.buildUpon(), contactId).appendPath(
-                Contacts.Photo.CONTENT_DIRECTORY).toString();
+        return ContentUris.appendId(CORP_CONTENT_URI.buildUpon(), contactId)
+                .appendPath(Contacts.Photo.CONTENT_DIRECTORY)
+                .toString();
     }
 
     /**
@@ -178,28 +181,33 @@
 
     @NonNull
     private static String getCorpLookupUriFromLookupKey(@NonNull String lookupKey, long contactId) {
-        return ContentUris.withAppendedId(Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI,
-                        ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey),
-                ENTERPRISE_CONTACT_ID_BASE + contactId).toString();
+        return ContentUris.withAppendedId(
+                        Uri.withAppendedPath(
+                                Contacts.CONTENT_LOOKUP_URI,
+                                ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey),
+                        ENTERPRISE_CONTACT_ID_BASE + contactId)
+                .toString();
     }
 
     @NonNull
     private static String getCorpLookupUriFromLookupKey(@NonNull String lookupKey) {
-        return Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI,
-                ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey).toString();
+        return Uri.withAppendedPath(
+                        Contacts.CONTENT_LOOKUP_URI, ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey)
+                .toString();
     }
 
     /**
      * Transforms a {@link SearchSpec} through its builder, adding property filters and projections
      * that restrict the allowed properties for the {@link Person} schema type.
      */
-    static void transformSearchSpec(@NonNull SearchSpec searchSpec,
-            @NonNull SearchSpec.Builder builder) {
+    static void transformSearchSpec(
+            @NonNull SearchSpec searchSpec, @NonNull SearchSpec.Builder builder) {
         Map<String, List<String>> projections = searchSpec.getProjections();
         Map<String, List<String>> filterProperties = searchSpec.getFilterProperties();
-        builder.addProjection(Person.SCHEMA_TYPE,
-                getAccessibleProperties(projections.get(Person.SCHEMA_TYPE)));
-        builder.addFilterProperties(Person.SCHEMA_TYPE,
+        builder.addProjection(
+                Person.SCHEMA_TYPE, getAccessibleProperties(projections.get(Person.SCHEMA_TYPE)));
+        builder.addFilterProperties(
+                Person.SCHEMA_TYPE,
                 getAccessibleProperties(filterProperties.get(Person.SCHEMA_TYPE)));
     }
 
@@ -209,8 +217,8 @@
      * intersection of the original properties and the allowed properties.
      */
     static void transformPropertiesMap(@NonNull Map<String, List<String>> propertiesMap) {
-        propertiesMap.put(Person.SCHEMA_TYPE,
-                getAccessibleProperties(propertiesMap.get(Person.SCHEMA_TYPE)));
+        propertiesMap.put(
+                Person.SCHEMA_TYPE, getAccessibleProperties(propertiesMap.get(Person.SCHEMA_TYPE)));
     }
 
     /**
diff --git a/service/java/com/android/server/appsearch/util/AdbDumpUtil.java b/service/java/com/android/server/appsearch/util/AdbDumpUtil.java
index e0f5249..efe96c6 100644
--- a/service/java/com/android/server/appsearch/util/AdbDumpUtil.java
+++ b/service/java/com/android/server/appsearch/util/AdbDumpUtil.java
@@ -33,10 +33,8 @@
 import java.security.NoSuchAlgorithmException;
 import java.util.Objects;
 
-/**
- * A utility class for helper methods to process {@link DebugInfoProto}.
- */
-public class AdbDumpUtil {
+/** A utility class for helper methods to process {@link DebugInfoProto}. */
+public final class AdbDumpUtil {
     private static final String TAG = "AppSearchAdbDumpUtil";
     private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
 
@@ -124,4 +122,6 @@
         debugInfoBuilder.setSchemaInfo(schemaInfoBuilder);
         return debugInfoBuilder.build();
     }
+
+    private AdbDumpUtil() {}
 }
diff --git a/service/java/com/android/server/appsearch/util/ApiCallRecord.java b/service/java/com/android/server/appsearch/util/ApiCallRecord.java
index abdffe8..3f654a2 100644
--- a/service/java/com/android/server/appsearch/util/ApiCallRecord.java
+++ b/service/java/com/android/server/appsearch/util/ApiCallRecord.java
@@ -26,24 +26,18 @@
 
 import java.util.Objects;
 
-/**
- * A class that wraps basic information of AppSearch API calls for dumpsys.
- */
+/** A class that wraps basic information of AppSearch API calls for dumpsys. */
 public class ApiCallRecord {
     // The time when the API call is logged, in the form of the milliseconds since boot.
     private final long mTimeMillis;
 
-    @CallStats.CallType
-    private final int mCallType;
+    @CallStats.CallType private final int mCallType;
 
-    @Nullable
-    private final String mPackageName;
+    @Nullable private final String mPackageName;
 
-    @Nullable
-    private final String mDatabaseName;
+    @Nullable private final String mDatabaseName;
 
-    @AppSearchResult.ResultCode
-    private final int mStatusCode;
+    @AppSearchResult.ResultCode private final int mStatusCode;
 
     private final int mTotalLatencyMillis;
 
@@ -152,8 +146,8 @@
             builder.append(", PackageName: ").append(mPackageName);
         }
         if (mDatabaseName != null) {
-            builder.append(", DatabaseName: ").append(
-                    AdbDumpUtil.generateFingerprintMd5(mDatabaseName));
+            builder.append(", DatabaseName: ")
+                    .append(AdbDumpUtil.generateFingerprintMd5(mDatabaseName));
         }
         builder.append(", StatusCode: ").append(mStatusCode);
         builder.append(", TotalLatencyMillis: ").append(mTotalLatencyMillis);
diff --git a/service/java/com/android/server/appsearch/util/ExceptionUtil.java b/service/java/com/android/server/appsearch/util/ExceptionUtil.java
deleted file mode 100644
index 9126220..0000000
--- a/service/java/com/android/server/appsearch/util/ExceptionUtil.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2023 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 com.android.server.appsearch.util;
-
-/**
- * Utilities for handling exceptions.
- *
- * @hide
- */
-public final class ExceptionUtil {
-
-  /**
-   * {@link RuntimeException} will be rethrown if {@link #isItOkayToRethrowException()}
-   * returns true.
-   */
-  public static final void handleException(Exception e) {
-    if (isItOkayToRethrowException() && e instanceof RuntimeException) {
-      rethrowRuntimeException((RuntimeException) e);
-    }
-  }
-
-  /** Returns whether it is OK to rethrow exceptions from this entrypoint. */
-  private static final boolean isItOkayToRethrowException() {
-    return false;
-  }
-
-  /**
-   * A helper method to rethrow {@link RuntimeException}.
-   *
-   * <p>We use this to enforce exception type and assure the compiler/linter that the exception is
-   * indeed {@link RuntimeException} and can be rethrown safely.
-   */
-  private static final void rethrowRuntimeException(RuntimeException e) {
-    throw e;
-  }
-
-  private ExceptionUtil() {}
-}
diff --git a/service/java/com/android/server/appsearch/util/ExecutorManager.java b/service/java/com/android/server/appsearch/util/ExecutorManager.java
index 5f10513..5503ff9 100644
--- a/service/java/com/android/server/appsearch/util/ExecutorManager.java
+++ b/service/java/com/android/server/appsearch/util/ExecutorManager.java
@@ -22,9 +22,9 @@
 import android.util.ArrayMap;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.appsearch.FrameworkAppSearchConfig;
-import com.android.server.appsearch.FrameworkAppSearchConfigImpl;
 import com.android.server.appsearch.AppSearchRateLimitConfig;
+import com.android.server.appsearch.FrameworkServiceAppSearchConfig;
+import com.android.server.appsearch.ServiceAppSearchConfig;
 
 import java.util.Map;
 import java.util.Objects;
@@ -41,7 +41,7 @@
  * @hide
  */
 public class ExecutorManager {
-    private final FrameworkAppSearchConfig mAppSearchConfig;
+    private final ServiceAppSearchConfig mAppSearchConfig;
 
     /**
      * A map of per-user executors for queued work. These can be started or shut down via this
@@ -53,34 +53,35 @@
     /**
      * Creates a new {@link ExecutorService} with default settings for use in AppSearch.
      *
-     * <p>The default settings are to use as many threads as there are CPUs. The core pool size is
-     * 1 if cached executors should be used, or also the CPU number if fixed executors should be
-     * used.
+     * <p>The default settings are to use as many threads as there are CPUs. The core pool size is 1
+     * if cached executors should be used, or also the CPU number if fixed executors should be used.
      */
     @NonNull
     public static ExecutorService createDefaultExecutorService() {
-        boolean useFixedExecutorService = FrameworkAppSearchConfigImpl.getUseFixedExecutorService();
+        boolean useFixedExecutorService =
+                FrameworkServiceAppSearchConfig.getUseFixedExecutorService();
         int corePoolSize = useFixedExecutorService ? Runtime.getRuntime().availableProcessors() : 1;
         long keepAliveTime = useFixedExecutorService ? 0L : 60L;
 
-        return AppSearchEnvironmentFactory.getEnvironmentInstance().createExecutorService(
-                /*corePoolSize=*/ corePoolSize,
-                /*maxConcurrency=*/ Runtime.getRuntime().availableProcessors(),
-                /*keepAliveTime=*/ keepAliveTime,
-                /*unit=*/ TimeUnit.SECONDS,
-                /*workQueue=*/ new LinkedBlockingQueue<>(),
-                /*priority=*/ 0); // priority is unused.
+        return AppSearchEnvironmentFactory.getEnvironmentInstance()
+                .createExecutorService(
+                        /* corePoolSize= */ corePoolSize,
+                        /* maxConcurrency= */ Runtime.getRuntime().availableProcessors(),
+                        /* keepAliveTime= */ keepAliveTime,
+                        /* unit= */ TimeUnit.SECONDS,
+                        /* workQueue= */ new LinkedBlockingQueue<>(),
+                        /* priority= */ 0); // priority is unused.
     }
 
-    public ExecutorManager(@NonNull FrameworkAppSearchConfig appSearchConfig) {
+    public ExecutorManager(@NonNull ServiceAppSearchConfig appSearchConfig) {
         mAppSearchConfig = Objects.requireNonNull(appSearchConfig);
     }
 
     /**
      * Gets the executor service for the given user, creating it if it does not exist.
      *
-     * <p> If AppSearch rate limiting is enabled, the input rate Limit config will be non-null,
-     * and the returned executor will be a RateLimitedExecutor instance.
+     * <p>If AppSearch rate limiting is enabled, the input rate Limit config will be non-null, and
+     * the returned executor will be a RateLimitedExecutor instance.
      *
      * <p>You are responsible for making sure not to call this for locked users. The executor will
      * be created without problems but most operations on locked users will fail.
@@ -90,8 +91,8 @@
         Objects.requireNonNull(userHandle);
         synchronized (mPerUserExecutorsLocked) {
             if (mAppSearchConfig.getCachedRateLimitEnabled()) {
-                return getOrCreateUserRateLimitedExecutorLocked(userHandle,
-                        mAppSearchConfig.getCachedRateLimitConfig());
+                return getOrCreateUserRateLimitedExecutorLocked(
+                        userHandle, mAppSearchConfig.getCachedRateLimitConfig());
             } else {
                 return getOrCreateUserExecutorLocked(userHandle);
             }
@@ -114,16 +115,17 @@
 
     @GuardedBy("mPerUserExecutorsLocked")
     @NonNull
-    private Executor getOrCreateUserRateLimitedExecutorLocked(@NonNull UserHandle userHandle,
-            @NonNull AppSearchRateLimitConfig rateLimitConfig) {
+    private Executor getOrCreateUserRateLimitedExecutorLocked(
+            @NonNull UserHandle userHandle, @NonNull AppSearchRateLimitConfig rateLimitConfig) {
         Objects.requireNonNull(userHandle);
         Objects.requireNonNull(rateLimitConfig);
         ExecutorService executor = mPerUserExecutorsLocked.get(userHandle);
         if (executor instanceof RateLimitedExecutor) {
             ((RateLimitedExecutor) executor).setRateLimitConfig(rateLimitConfig);
         } else {
-            executor = new RateLimitedExecutor(ExecutorManager.createDefaultExecutorService(),
-                    rateLimitConfig);
+            executor =
+                    new RateLimitedExecutor(
+                            ExecutorManager.createDefaultExecutorService(), rateLimitConfig);
             mPerUserExecutorsLocked.put(userHandle, executor);
         }
         return executor;
diff --git a/service/java/com/android/server/appsearch/util/PackageManagerUtil.java b/service/java/com/android/server/appsearch/util/PackageManagerUtil.java
index abc6a8c..76235e9 100644
--- a/service/java/com/android/server/appsearch/util/PackageManagerUtil.java
+++ b/service/java/com/android/server/appsearch/util/PackageManagerUtil.java
@@ -45,7 +45,7 @@
      * @param packageName package whose signing certificates to check
      * @param sha256cert sha256 of the signing certificate for which to search
      * @return true if this package was or is signed by exactly the certificate with SHA-256 as
-     *         {@code sha256cert}
+     *     {@code sha256cert}
      */
     public static boolean hasSigningCertificate(
             Context context, String packageName, byte[] sha256cert) {
@@ -53,8 +53,7 @@
             return hasSigningCertificateBelowP(context, packageName, sha256cert);
         }
 
-        return context
-                .getPackageManager()
+        return context.getPackageManager()
                 .hasSigningCertificate(packageName, sha256cert, PackageManager.CERT_INPUT_SHA256);
     }
 
@@ -62,8 +61,9 @@
             Context context, String packageName, byte[] sha256cert) {
         PackageInfo packageInfo;
         try {
-            packageInfo = context.getPackageManager()
-                    .getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+            packageInfo =
+                    context.getPackageManager()
+                            .getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
         } catch (NameNotFoundException e) {
             throw new IllegalArgumentException("Given package does not exist on device!");
         }
@@ -77,7 +77,8 @@
         try {
             Signature[] signatures = packageInfo.signatures;
             if (signatures != null && signatures.length == 1) {
-                byte[] certificate = MessageDigest.getInstance(/* algorithm= */ "SHA-256")
+                byte[] certificate =
+                        MessageDigest.getInstance(/* algorithm= */ "SHA-256")
                                 .digest(signatures[0].toByteArray());
                 return Arrays.equals(certificate, sha256cert);
             }
diff --git a/service/java/com/android/server/appsearch/util/PackageUtil.java b/service/java/com/android/server/appsearch/util/PackageUtil.java
index 714ffb6..fddf7ec 100644
--- a/service/java/com/android/server/appsearch/util/PackageUtil.java
+++ b/service/java/com/android/server/appsearch/util/PackageUtil.java
@@ -21,24 +21,28 @@
 import android.content.pm.PackageManager;
 import android.os.Process;
 
+
 /**
- * Utilities for interacting with {@link android.content.pm.PackageManager},
- * {@link android.os.UserHandle}, and other parts of dealing with apps and binder.
+ * Utilities for interacting with {@link android.content.pm.PackageManager}, {@link
+ * android.os.UserHandle}, and other parts of dealing with apps and binder.
  *
  * @hide
  */
 public class PackageUtil {
+
+    public static final int INVALID_UID = Process.INVALID_UID;
+
     private PackageUtil() {}
 
     /**
-     * Finds the UID of the {@code packageName} in the given {@code context}. Returns
-     * {@link Process#INVALID_UID} if unable to find the UID.
+     * Finds the UID of the {@code packageName} in the given {@code context}. Returns {@link
+     * Process#INVALID_UID} if unable to find the UID.
      */
     public static int getPackageUid(@NonNull Context context, @NonNull String packageName) {
         try {
-            return context.getPackageManager().getPackageUid(packageName, /*flags=*/ 0);
+            return context.getPackageManager().getPackageUid(packageName, /* flags= */ 0);
         } catch (PackageManager.NameNotFoundException e) {
-            return Process.INVALID_UID;
+            return INVALID_UID;
         }
     }
 }
diff --git a/service/java/com/android/server/appsearch/util/RateLimitedExecutor.java b/service/java/com/android/server/appsearch/util/RateLimitedExecutor.java
index e7fce0f..fea1cdb 100644
--- a/service/java/com/android/server/appsearch/util/RateLimitedExecutor.java
+++ b/service/java/com/android/server/appsearch/util/RateLimitedExecutor.java
@@ -35,42 +35,35 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+/** An implementation of {@link ExecutorService} for AppSearch's per-package task queue. */
 public class RateLimitedExecutor implements ExecutorService {
     private static final String TAG = "AppSearchRateLimitExec";
     private final ExecutorService mExecutor;
 
-    /**
-     * Lock needed for operations in this class.
-     */
+    /** Lock needed for operations in this class. */
     private final Object mLock = new Object();
 
     /**
-     * A map of packageName -> {@link TaskCostInfo} of package task count and cost currently on
-     * task queue.
+     * A map of packageName -> {@link TaskCostInfo} of package task count and cost currently on task
+     * queue.
      */
     @GuardedBy("mLock")
     private final ArrayMap<String, TaskCostInfo> mPerPackageTaskCostsLocked = new ArrayMap<>();
 
-    /**
-     * The {@link AppSearchRateLimitConfig} for the executor
-     */
+    /** The {@link AppSearchRateLimitConfig} for the executor */
     @GuardedBy("mLock")
     private AppSearchRateLimitConfig mRateLimitConfigLocked;
 
-    /**
-     * Keeps track of the task queue size.
-     */
+    /** Keeps track of the task queue size. */
     @GuardedBy("mLock")
     private int mTaskQueueSizeLocked;
 
-    /**
-     * Sum of costs of all tasks currently on the executor queue.
-     */
+    /** Sum of costs of all tasks currently on the executor queue. */
     @GuardedBy("mLock")
     private int mTaskQueueTotalCostLocked;
 
-    public RateLimitedExecutor(@NonNull ExecutorService executor,
-            @NonNull AppSearchRateLimitConfig rateLimitConfig) {
+    public RateLimitedExecutor(
+            @NonNull ExecutorService executor, @NonNull AppSearchRateLimitConfig rateLimitConfig) {
         mExecutor = Objects.requireNonNull(executor);
         mRateLimitConfigLocked = Objects.requireNonNull(rateLimitConfig);
         mTaskQueueSizeLocked = 0;
@@ -81,26 +74,30 @@
      * Returns true and executes the runnable if it can be accepted by the rate-limited executor.
      * Otherwise returns false.
      *
-     * @param lambda        The lambda to execute on the rate-limited executor.
-     * @param packageName   Package making this lambda call.
-     * @param apiType       Api type of this lambda call.
+     * @param lambda The lambda to execute on the rate-limited executor.
+     * @param packageName Package making this lambda call.
+     * @param apiType Api type of this lambda call.
      */
-    public boolean execute(@NonNull Runnable lambda, @NonNull String packageName,
+    public boolean execute(
+            @NonNull Runnable lambda,
+            @NonNull String packageName,
             @CallStats.CallType int apiType) {
         Objects.requireNonNull(lambda);
         Objects.requireNonNull(packageName);
         if (!addTaskToQueue(packageName, apiType)) {
             return false;
         }
-        mExecutor.execute(() -> {
-            try {
-                lambda.run();
-            } finally {
-                removeTaskFromQueue(packageName, apiType);
-            }
-        });
+        mExecutor.execute(
+                () -> {
+                    try {
+                        lambda.run();
+                    } finally {
+                        removeTaskFromQueue(packageName, apiType);
+                    }
+                });
         return true;
     }
+
     @NonNull
     public ExecutorService getExecutor() {
         return mExecutor;
@@ -113,7 +110,6 @@
         }
     }
 
-
     @VisibleForTesting
     @NonNull
     public ArrayMap<String, TaskCostInfo> getPerPackageTaskCosts() {
@@ -122,9 +118,7 @@
         }
     }
 
-    /**
-     * Sets the rate limit config for this rate limited executor.
-     */
+    /** Sets the rate limit config for this rate limited executor. */
     public void setRateLimitConfig(@NonNull AppSearchRateLimitConfig rateLimitConfigLocked) {
         synchronized (mLock) {
             mRateLimitConfigLocked = Objects.requireNonNull(rateLimitConfigLocked);
@@ -144,8 +138,8 @@
                     packageTaskCostInfo == null ? 0 : packageTaskCostInfo.mTotalTaskCost;
             int apiCost = mRateLimitConfigLocked.getApiCost(apiType);
             if (totalPackageApiCost + apiCost
-                    > mRateLimitConfigLocked.getTaskQueuePerPackageCapacity() ||
-                    mTaskQueueTotalCostLocked + apiCost
+                            > mRateLimitConfigLocked.getTaskQueuePerPackageCapacity()
+                    || mTaskQueueTotalCostLocked + apiCost
                             > mRateLimitConfigLocked.getTaskQueueTotalCapacity()) {
                 return false;
             } else {
@@ -158,15 +152,16 @@
     }
 
     /**
-     * Removes a task from the executor queue by decrementing the count for the task's package
-     * and api type.
+     * Removes a task from the executor queue by decrementing the count for the task's package and
+     * api type.
      */
     @VisibleForTesting
     public void removeTaskFromQueue(@NonNull String packageName, @CallStats.CallType int apiType) {
         synchronized (mLock) {
             Objects.requireNonNull(packageName);
             if (!mPerPackageTaskCostsLocked.containsKey(packageName)) {
-                Log.e(TAG,
+                Log.e(
+                        TAG,
                         "There are no tasks to remove from the queue for package: " + packageName);
                 return;
             }
@@ -254,8 +249,9 @@
     }
 
     @Override
-    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,
-            TimeUnit unit) throws InterruptedException {
+    public <T> List<Future<T>> invokeAll(
+            Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+            throws InterruptedException {
         return mExecutor.invokeAll(tasks, timeout, unit);
     }
 
@@ -271,9 +267,7 @@
         return mExecutor.invokeAny(tasks, timeout, unit);
     }
 
-    /**
-     * Class containing the integer pair of task count and total task costs.
-     */
+    /** Class containing the integer pair of task count and total task costs. */
     public static final class TaskCostInfo {
         public int mTaskCount;
         public int mTotalTaskCost;
diff --git a/service/java/com/android/server/appsearch/util/ServiceImplHelper.java b/service/java/com/android/server/appsearch/util/ServiceImplHelper.java
index 5378f77..f3e0f00 100644
--- a/service/java/com/android/server/appsearch/util/ServiceImplHelper.java
+++ b/service/java/com/android/server/appsearch/util/ServiceImplHelper.java
@@ -22,7 +22,7 @@
 import android.annotation.BinderThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.appsearch.AppSearchBatchResult;
+import android.app.admin.DevicePolicyManager;
 import android.app.appsearch.AppSearchEnvironmentFactory;
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.aidl.AppSearchAttributionSource;
@@ -50,6 +50,7 @@
 
 /**
  * Utilities to help with implementing AppSearch's services.
+ *
  * @hide
  */
 public class ServiceImplHelper {
@@ -57,6 +58,7 @@
 
     private final Context mContext;
     private final UserManager mUserManager;
+    private final DevicePolicyManager mDevicePolicyManager;
     private final ExecutorManager mExecutorManager;
     private final AppSearchUserInstanceManager mAppSearchUserInstanceManager;
 
@@ -81,12 +83,12 @@
         mUserManager = context.getSystemService(UserManager.class);
         mExecutorManager = Objects.requireNonNull(executorManager);
         mAppSearchUserInstanceManager = AppSearchUserInstanceManager.getInstance();
+        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
     }
 
     public void setUserIsLocked(@NonNull UserHandle userHandle, boolean isLocked) {
         boolean isManagedProfile = mUserManager.isManagedProfile(userHandle.getIdentifier());
-        UserHandle parentUser = isManagedProfile ? mUserManager.getProfileParent(userHandle)
-                : null;
+        UserHandle parentUser = isManagedProfile ? mUserManager.getProfileParent(userHandle) : null;
         synchronized (mUnlockedUsersLocked) {
             if (isLocked) {
                 if (isManagedProfile) {
@@ -145,7 +147,7 @@
      * <p>This method must be called on the binder thread.
      *
      * @return The result containing the final verified user that the call should run as, if all
-     * checks pass. Otherwise return null.
+     *     checks pass. Otherwise return null.
      */
     @BinderThread
     @Nullable
@@ -156,7 +158,9 @@
         try {
             return verifyIncomingCall(callerAttributionSource, userHandle);
         } catch (Throwable t) {
-            invokeCallbackOnResult(errorCallback, throwableToFailedResult(t));
+            AppSearchResult failedResult = throwableToFailedResult(t);
+            invokeCallbackOnResult(
+                    errorCallback, AppSearchResultParcel.fromFailedResult(failedResult));
             return null;
         }
     }
@@ -170,7 +174,7 @@
      * <p>This method must be called on the binder thread.
      *
      * @return The result containing the final verified user that the call should run as, if all
-     * checks pass. Otherwise return null.
+     *     checks pass. Otherwise, return null.
      */
     @BinderThread
     @Nullable
@@ -208,10 +212,9 @@
         long callingIdentity = Binder.clearCallingIdentity();
         try {
             verifyCaller(callingUid, callerAttributionSource);
-            String callingPackageName =
-                Objects.requireNonNull(callerAttributionSource.getPackageName());
+            String callingPackageName = callerAttributionSource.getPackageName();
             UserHandle targetUser =
-                handleIncomingUser(callingPackageName, userHandle, callingPid, callingUid);
+                    handleIncomingUser(callingPackageName, userHandle, callingPid, callingUid);
             verifyUserUnlocked(targetUser);
             return targetUser;
         } finally {
@@ -221,20 +224,20 @@
 
     /**
      * Verify various aspects of the calling user.
+     *
      * @param callingUid Uid of the caller, usually retrieved from Binder for authenticity.
      * @param callerAttributionSource The permission identity of the caller
      */
     // enforceCallingUidAndPid is called on AttributionSource during deserialization.
-    private void verifyCaller(int callingUid,
-            @NonNull AppSearchAttributionSource callerAttributionSource) {
+    private void verifyCaller(
+            int callingUid, @NonNull AppSearchAttributionSource callerAttributionSource) {
         // Obtain the user where the client is running in. Note that this could be different from
         // the userHandle where the client wants to run the AppSearch operation in.
         UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid);
-        Context callingUserContext = AppSearchEnvironmentFactory
-            .getEnvironmentInstance()
-            .createContextAsUser(mContext, callingUserHandle);
-        String callingPackageName =
-            Objects.requireNonNull(callerAttributionSource.getPackageName());
+        Context callingUserContext =
+                AppSearchEnvironmentFactory.getEnvironmentInstance()
+                        .createContextAsUser(mContext, callingUserHandle);
+        String callingPackageName = callerAttributionSource.getPackageName();
         verifyCallingPackage(callingUserContext, callingUid, callingPackageName);
         verifyNotInstantApp(callingUserContext, callingPackageName);
     }
@@ -248,8 +251,8 @@
             @NonNull Context actualCallingUserContext,
             int actualCallingUid,
             @NonNull String claimedCallingPackage) {
-        int claimedCallingUid = PackageUtil.getPackageUid(
-                actualCallingUserContext, claimedCallingPackage);
+        int claimedCallingUid =
+                PackageUtil.getPackageUid(actualCallingUserContext, claimedCallingPackage);
         if (claimedCallingUid != actualCallingUid) {
             throw new SecurityException(
                     "Specified calling package ["
@@ -267,8 +270,12 @@
     private void verifyNotInstantApp(@NonNull Context userContext, @NonNull String packageName) {
         PackageManager callingPackageManager = userContext.getPackageManager();
         if (callingPackageManager.isInstantApp(packageName)) {
-            throw new SecurityException("Caller not allowed to create AppSearch session"
-                    + "; userHandle=" + userContext.getUser() + ", callingPackage=" + packageName);
+            throw new SecurityException(
+                    "Caller not allowed to create AppSearch session"
+                            + "; userHandle="
+                            + userContext.getUser()
+                            + ", callingPackage="
+                            + packageName);
         }
     }
 
@@ -282,16 +289,18 @@
      * @param targetUserHandle The user which the caller is requesting to execute as.
      * @param callingPid The actual pid of the caller as determined by Binder.
      * @param callingUid The actual uid of the caller as determined by Binder.
-     *
      * @return the user handle that the call should run as. Will always be a concrete user.
-     *
      * @throws IllegalArgumentException if the target user is a special user.
-     * @throws SecurityException if caller trying to interact across user without
-     * {@link Manifest.permission#INTERACT_ACROSS_USERS_FULL}
+     * @throws SecurityException if caller trying to interact across user without {@link
+     *     Manifest.permission#INTERACT_ACROSS_USERS_FULL}
      */
+    @CanIgnoreReturnValue
     @NonNull
-    private UserHandle handleIncomingUser(@NonNull String callingPackageName,
-            @NonNull UserHandle targetUserHandle, int callingPid, int callingUid) {
+    private UserHandle handleIncomingUser(
+            @NonNull String callingPackageName,
+            @NonNull UserHandle targetUserHandle,
+            int callingPid,
+            int callingUid) {
         UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid);
         if (callingUserHandle.equals(targetUserHandle)) {
             return targetUserHandle;
@@ -304,26 +313,31 @@
         }
 
         if (mContext.checkPermission(
-                Manifest.permission.INTERACT_ACROSS_USERS_FULL,
-                callingPid,
-                callingUid) == PackageManager.PERMISSION_GRANTED) {try {
-            // Normally if the calling package doesn't exist in the target user, user cannot
-            // call AppSearch. But since the SDK side cannot be trusted, we still need to verify
-            // the calling package exists in the target user.
-            // We need to create the package context for the targetUser, and this call will fail
-            // if the calling package doesn't exist in the target user.
-            mContext.createPackageContextAsUser(callingPackageName, /*flags=*/0,
-                    targetUserHandle);
-        } catch (PackageManager.NameNotFoundException e) {
-            throw new SecurityException(
-                    "Package: " + callingPackageName + " haven't installed for user "
-                            + targetUserHandle.getIdentifier());
-        }
+                        Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingPid, callingUid)
+                == PackageManager.PERMISSION_GRANTED) {
+            try {
+                // Normally if the calling package doesn't exist in the target user, user cannot
+                // call AppSearch. But since the SDK side cannot be trusted, we still need to verify
+                // the calling package exists in the target user.
+                // We need to create the package context for the targetUser, and this call will fail
+                // if the calling package doesn't exist in the target user.
+                mContext.createPackageContextAsUser(
+                        callingPackageName, /* flags= */ 0, targetUserHandle);
+            } catch (PackageManager.NameNotFoundException e) {
+                throw new SecurityException(
+                        "Package: "
+                                + callingPackageName
+                                + " haven't installed for user "
+                                + targetUserHandle.getIdentifier());
+            }
             return targetUserHandle;
         }
         throw new SecurityException(
-                "Permission denied while calling from uid " + callingUid
-                        + " with " + targetUserHandle + "; Requires permission: "
+                "Permission denied while calling from uid "
+                        + callingUid
+                        + " with "
+                        + targetUserHandle
+                        + "; Requires permission: "
                         + Manifest.permission.INTERACT_ACROSS_USERS_FULL);
     }
 
@@ -333,14 +347,13 @@
      *
      * <p>You should first make sure the call is allowed to run using {@link #verifyCaller}.
      *
-     * @param targetUser            The verified user the call should run as, as determined by
-     *                              {@link #verifyCaller}.
-     * @param errorCallback         Callback to complete with an error if starting the lambda fails.
-     *                              Otherwise this callback is not triggered.
-     * @param callingPackageName    Package making this lambda call.
-     * @param apiType               Api type of this lambda call.
-     * @param lambda                The lambda to execute on the user-provided executor.
-     *
+     * @param targetUser The verified user the call should run as, as determined by {@link
+     *     #verifyCaller}.
+     * @param errorCallback Callback to complete with an error if starting the lambda fails.
+     *     Otherwise this callback is not triggered.
+     * @param callingPackageName Package making this lambda call.
+     * @param apiType Api type of this lambda call.
+     * @param lambda The lambda to execute on the user-provided executor.
      * @return true if the call is accepted by the executor and false otherwise.
      */
     @BinderThread
@@ -358,19 +371,24 @@
         try {
             Executor executor = mExecutorManager.getOrCreateUserExecutor(targetUser);
             if (executor instanceof RateLimitedExecutor) {
-                boolean callAccepted = ((RateLimitedExecutor) executor).execute(lambda,
-                        callingPackageName, apiType);
+                boolean callAccepted =
+                        ((RateLimitedExecutor) executor)
+                                .execute(lambda, callingPackageName, apiType);
                 if (!callAccepted) {
-                    invokeCallbackOnResult(errorCallback,
-                            AppSearchResult.newFailedResult(RESULT_RATE_LIMITED,
-                                    "AppSearch rate limit reached."));
+                    invokeCallbackOnResult(
+                            errorCallback,
+                            AppSearchResultParcel.fromFailedResult(
+                                    AppSearchResult.newFailedResult(
+                                            RESULT_RATE_LIMITED, "AppSearch rate limit reached.")));
                     return false;
                 }
             } else {
                 executor.execute(lambda);
             }
         } catch (RuntimeException e) {
-            invokeCallbackOnResult(errorCallback, throwableToFailedResult(e));
+            AppSearchResult failedResult = throwableToFailedResult(e);
+            invokeCallbackOnResult(
+                    errorCallback, AppSearchResultParcel.fromFailedResult(failedResult));
         }
         return true;
     }
@@ -381,14 +399,13 @@
      *
      * <p>You should first make sure the call is allowed to run using {@link #verifyCaller}.
      *
-     * @param targetUser            The verified user the call should run as, as determined by
-     *                              {@link #verifyCaller}.
-     * @param errorCallback         Callback to complete with an error if starting the lambda fails.
-     *                              Otherwise this callback is not triggered.
-     * @param callingPackageName    Package making this lambda call.
-     * @param apiType               Api type of this lambda call.
-     * @param lambda                The lambda to execute on the user-provided executor.
-     *
+     * @param targetUser The verified user the call should run as, as determined by {@link
+     *     #verifyCaller}.
+     * @param errorCallback Callback to complete with an error if starting the lambda fails.
+     *     Otherwise this callback is not triggered.
+     * @param callingPackageName Package making this lambda call.
+     * @param apiType Api type of this lambda call.
+     * @param lambda The lambda to execute on the user-provided executor.
      * @return true if the call is accepted by the executor and false otherwise.
      */
     @BinderThread
@@ -405,12 +422,14 @@
         try {
             Executor executor = mExecutorManager.getOrCreateUserExecutor(targetUser);
             if (executor instanceof RateLimitedExecutor) {
-                boolean callAccepted = ((RateLimitedExecutor) executor).execute(lambda,
-                        callingPackageName, apiType);
+                boolean callAccepted =
+                        ((RateLimitedExecutor) executor)
+                                .execute(lambda, callingPackageName, apiType);
                 if (!callAccepted) {
-                    invokeCallbackOnError(errorCallback,
-                            AppSearchResult.newFailedResult(RESULT_RATE_LIMITED,
-                                    "AppSearch rate limit reached."));
+                    invokeCallbackOnError(
+                            errorCallback,
+                            AppSearchResult.newFailedResult(
+                                    RESULT_RATE_LIMITED, "AppSearch rate limit reached."));
                     return false;
                 }
             } else {
@@ -428,12 +447,11 @@
      *
      * <p>You should first make sure the call is allowed to run using {@link #verifyCaller}.
      *
-     * @param targetUser         The verified user the call should run as, as determined by
-     *                           {@link #verifyCaller}.
+     * @param targetUser The verified user the call should run as, as determined by {@link
+     *     #verifyCaller}.
      * @param callingPackageName Package making this lambda call.
-     * @param apiType            Api type of this lambda call.
-     * @param lambda             The lambda to execute on the user-provided executor.
-     *
+     * @param apiType Api type of this lambda call.
+     * @param lambda The lambda to execute on the user-provided executor.
      * @return true if the call is accepted by the executor and false otherwise.
      */
     @BinderThread
@@ -447,8 +465,7 @@
         Objects.requireNonNull(lambda);
         Executor executor = mExecutorManager.getOrCreateUserExecutor(targetUser);
         if (executor instanceof RateLimitedExecutor) {
-            return ((RateLimitedExecutor) executor).execute(lambda, callingPackageName,
-                    apiType);
+            return ((RateLimitedExecutor) executor).execute(lambda, callingPackageName, apiType);
         } else {
             executor.execute(lambda);
             return true;
@@ -467,18 +484,31 @@
         }
         UserHandle enterpriseUser = getEnterpriseUser(targetUser);
         // Do not return the enterprise user if its AppSearch instance does not exist
-        if (enterpriseUser == null ||
-                mAppSearchUserInstanceManager.getUserInstanceOrNull(enterpriseUser) == null) {
+        if (enterpriseUser == null
+                || mAppSearchUserInstanceManager.getUserInstanceOrNull(enterpriseUser) == null) {
             return null;
         }
         return enterpriseUser;
     }
 
-    /** Invokes the {@link IAppSearchResultCallback} with the result. */
-    public static void invokeCallbackOnResult(
-            IAppSearchResultCallback callback, AppSearchResult<?> result) {
+    /** Returns whether the given user is managed by an organization. */
+    public boolean isUserOrganizationManaged(@NonNull UserHandle targetUser) {
+        long token = Binder.clearCallingIdentity();
         try {
-            callback.onResult(new AppSearchResultParcel<>(result));
+            if (mDevicePolicyManager.isDeviceManaged()) {
+                return true;
+            }
+            return mUserManager.isManagedProfile(targetUser.getIdentifier());
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /** Invokes the {@link IAppSearchResultCallback} with the result parcel. */
+    public static void invokeCallbackOnResult(
+            IAppSearchResultCallback callback, AppSearchResultParcel<?> resultParcel) {
+        try {
+            callback.onResult(resultParcel);
         } catch (RemoteException e) {
             Log.e(TAG, "Unable to send result to the callback", e);
         }
@@ -486,9 +516,9 @@
 
     /** Invokes the {@link IAppSearchBatchResultCallback} with the result. */
     public static void invokeCallbackOnResult(
-            IAppSearchBatchResultCallback callback, AppSearchBatchResult<String, ?> result) {
+            IAppSearchBatchResultCallback callback, AppSearchBatchResultParcel<?> resultParcel) {
         try {
-            callback.onResult(new AppSearchBatchResultParcel<>(result));
+            callback.onResult(resultParcel);
         } catch (RemoteException e) {
             Log.e(TAG, "Unable to send result to the callback", e);
         }
@@ -504,13 +534,11 @@
         invokeCallbackOnError(callback, throwableToFailedResult(throwable));
     }
 
-    /**
-     * Invokes the {@link IAppSearchBatchResultCallback} with the error result.
-     */
+    /** Invokes the {@link IAppSearchBatchResultCallback} with the error result. */
     public static void invokeCallbackOnError(
             @NonNull IAppSearchBatchResultCallback callback, @NonNull AppSearchResult<?> result) {
         try {
-            callback.onSystemError(new AppSearchResultParcel<>(result));
+            callback.onSystemError(AppSearchResultParcel.fromFailedResult(result));
         } catch (RemoteException e) {
             Log.e(TAG, "Unable to send error to the callback", e);
         }
diff --git a/service/java/com/android/server/appsearch/visibilitystore/FrameworkCallerAccess.java b/service/java/com/android/server/appsearch/visibilitystore/FrameworkCallerAccess.java
index fa23aa8..c74e7e3 100644
--- a/service/java/com/android/server/appsearch/visibilitystore/FrameworkCallerAccess.java
+++ b/service/java/com/android/server/appsearch/visibilitystore/FrameworkCallerAccess.java
@@ -49,8 +49,9 @@
      */
     public FrameworkCallerAccess(
             @NonNull AppSearchAttributionSource callerAttributionSource,
-            boolean callerHasSystemAccess, boolean isForEnterprise) {
-        super(Objects.requireNonNull(callerAttributionSource.getPackageName()));
+            boolean callerHasSystemAccess,
+            boolean isForEnterprise) {
+        super(callerAttributionSource.getPackageName());
         mAttributionSource = callerAttributionSource;
         mCallerHasSystemAccess = callerHasSystemAccess;
         mIsForEnterprise = isForEnterprise;
@@ -85,8 +86,12 @@
 
     @Override
     public boolean equals(@Nullable Object o) {
-        if (this == o) return true;
-        if (!(o instanceof FrameworkCallerAccess)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof FrameworkCallerAccess)) {
+            return false;
+        }
         FrameworkCallerAccess that = (FrameworkCallerAccess) o;
         return super.equals(o)
                 && mCallerHasSystemAccess == that.mCallerHasSystemAccess
@@ -96,7 +101,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), mAttributionSource, mCallerHasSystemAccess,
-                mIsForEnterprise);
+        return Objects.hash(
+                super.hashCode(), mAttributionSource, mCallerHasSystemAccess, mIsForEnterprise);
     }
 }
diff --git a/service/java/com/android/server/appsearch/visibilitystore/PolicyCheckerImpl.java b/service/java/com/android/server/appsearch/visibilitystore/PolicyCheckerImpl.java
index 5380c15..cc4e42a 100644
--- a/service/java/com/android/server/appsearch/visibilitystore/PolicyCheckerImpl.java
+++ b/service/java/com/android/server/appsearch/visibilitystore/PolicyCheckerImpl.java
@@ -49,8 +49,8 @@
         // https://cs.android.com/android/platform/superproject/main/+/main:packages/providers/ContactsProvider/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuard.java;l=81;drc=242bb9f25b210fbfe36a384088221b54b2602b34
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             // This api is only supported on U+
-            return mDevicePolicyManager.hasManagedProfileContactsAccess(mUserContext.getUser(),
-                    callingPackageName);
+            return mDevicePolicyManager.hasManagedProfileContactsAccess(
+                    mUserContext.getUser(), callingPackageName);
         }
         // Below U, we should call
         // DevicePolicyManager#getCrossProfileContactsSearchDisabled(UserHandle) to check if the
diff --git a/service/java/com/android/server/appsearch/visibilitystore/VisibilityCheckerImpl.java b/service/java/com/android/server/appsearch/visibilitystore/VisibilityCheckerImpl.java
index fe492c9..093190b 100644
--- a/service/java/com/android/server/appsearch/visibilitystore/VisibilityCheckerImpl.java
+++ b/service/java/com/android/server/appsearch/visibilitystore/VisibilityCheckerImpl.java
@@ -89,10 +89,11 @@
         // If caller requires enterprise access, the given schema is only visible if caller has all
         // required permissions.
         if (frameworkCallerAccess.isForEnterprise()) {
-            return internalVisibilityConfig != null && isSchemaVisibleToPermission(
-                    internalVisibilityConfig.getVisibilityConfig(),
-                    frameworkCallerAccess.getCallingAttributionSource(),
-                    /*checkEnterpriseAccess=*/ true);
+            return internalVisibilityConfig != null
+                    && isSchemaVisibleToPermission(
+                            internalVisibilityConfig.getVisibilityConfig(),
+                            frameworkCallerAccess.getCallingAttributionSource(),
+                            /* checkEnterpriseAccess= */ true);
         }
 
         if (internalVisibilityConfig == null) {
@@ -103,16 +104,15 @@
 
         // Check whether the calling package has system access and the target schema is visible to
         // the system.
-        if (frameworkCallerAccess.doesCallerHaveSystemAccess() &&
-                !internalVisibilityConfig.isNotDisplayedBySystem()) {
+        if (frameworkCallerAccess.doesCallerHaveSystemAccess()
+                && !internalVisibilityConfig.isNotDisplayedBySystem()) {
             return true;
         }
 
         // Check OR visibility settings. Caller could access if they match ANY of the requirements
         // in the visibilityConfig.
         SchemaVisibilityConfig visibilityConfig = internalVisibilityConfig.getVisibilityConfig();
-        if (checkMatchAnyVisibilityConfig(
-                frameworkCallerAccess, visibilityConfig)) {
+        if (checkMatchAnyVisibilityConfig(frameworkCallerAccess, visibilityConfig)) {
             return true;
         }
 
@@ -121,28 +121,28 @@
         Set<SchemaVisibilityConfig> visibleToConfigs =
                 internalVisibilityConfig.getVisibleToConfigs();
         for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
-            if (checkMatchAllVisibilityConfig(
-                    frameworkCallerAccess, visibleToConfig)) {
+            if (checkMatchAllVisibilityConfig(frameworkCallerAccess, visibleToConfig)) {
                 return true;
             }
         }
         return false;
     }
 
-    /**  Check whether the caller math ANY of the visibility requirements.     */
+    /** Check whether the caller math ANY of the visibility requirements. */
     private boolean checkMatchAnyVisibilityConfig(
-        @NonNull FrameworkCallerAccess frameworkCallerAccess,
-        @NonNull SchemaVisibilityConfig visibilityConfig) {
-        if (isSchemaVisibleToPackages(visibilityConfig,
-            frameworkCallerAccess.getCallingAttributionSource().getUid())) {
+            @NonNull FrameworkCallerAccess frameworkCallerAccess,
+            @NonNull SchemaVisibilityConfig visibilityConfig) {
+        if (isSchemaVisibleToPackages(
+                visibilityConfig, frameworkCallerAccess.getCallingAttributionSource().getUid())) {
             // The caller is in the allow list and has access to the given schema.
             return true;
         }
 
         // Check whether caller has all required permissions for the given schema.
-        if(isSchemaVisibleToPermission(visibilityConfig,
+        if (isSchemaVisibleToPermission(
+                visibilityConfig,
                 frameworkCallerAccess.getCallingAttributionSource(),
-                /*checkEnterpriseAccess=*/ false)) {
+                /* checkEnterpriseAccess= */ false)) {
             return true;
         }
 
@@ -150,10 +150,10 @@
         return isSchemaPubliclyVisibleFromPackage(visibilityConfig, frameworkCallerAccess);
     }
 
-    /**  Check whether the caller math ALL of the visibility requirements.     */
+    /** Check whether the caller math ALL of the visibility requirements. */
     private boolean checkMatchAllVisibilityConfig(
-        @NonNull FrameworkCallerAccess frameworkCallerAccess,
-        @NonNull SchemaVisibilityConfig visibilityConfig) {
+            @NonNull FrameworkCallerAccess frameworkCallerAccess,
+            @NonNull SchemaVisibilityConfig visibilityConfig) {
 
         // We will skip following checks if user never specific them. But the caller should has
         // passed at least one check to get the access.
@@ -161,9 +161,10 @@
 
         // Check whether the caller is in the allow list and has access to the given schema.
         if (!visibilityConfig.getAllowedPackages().isEmpty()) {
-            if (!isSchemaVisibleToPackages(visibilityConfig,
-                frameworkCallerAccess.getCallingAttributionSource().getUid())) {
-                return false;// Return early for the 'ALL' case.
+            if (!isSchemaVisibleToPackages(
+                    visibilityConfig,
+                    frameworkCallerAccess.getCallingAttributionSource().getUid())) {
+                return false; // Return early for the 'ALL' case.
             }
             hasPassedCheck = true;
         }
@@ -171,10 +172,11 @@
         // Check whether caller has all required permissions for the given schema.
         // We could directly return the boolean results since it is the last checking.
         if (!visibilityConfig.getRequiredPermissions().isEmpty()) {
-            if (!isSchemaVisibleToPermission(visibilityConfig,
-                frameworkCallerAccess.getCallingAttributionSource(),
-                /*checkEnterpriseAccess=*/ false)) {
-                return false;// Return early for the 'ALL' case.
+            if (!isSchemaVisibleToPermission(
+                    visibilityConfig,
+                    frameworkCallerAccess.getCallingAttributionSource(),
+                    /* checkEnterpriseAccess= */ false)) {
+                return false; // Return early for the 'ALL' case.
             }
             hasPassedCheck = true;
         }
@@ -200,8 +202,10 @@
 
         // Ensure the sha 256 certificate matches the certificate of the actual publicly visible
         // target package.
-        if (!PackageManagerUtil.hasSigningCertificate(mUserContext,
-                targetPackage.getPackageName(), targetPackage.getSha256Certificate())) {
+        if (!PackageManagerUtil.hasSigningCertificate(
+                mUserContext,
+                targetPackage.getPackageName(),
+                targetPackage.getSha256Certificate())) {
             return false;
         }
 
@@ -211,9 +215,11 @@
         // the publicly visible target package for that schema could be the timer app package.
         try {
             // The call that opens up documents to "public" access
-            if (mUserContext.getPackageManager().canPackageQuery(
-                    frameworkCallerAccess.getCallingPackageName(),
-                    targetPackage.getPackageName())) {
+            if (mUserContext
+                    .getPackageManager()
+                    .canPackageQuery(
+                            frameworkCallerAccess.getCallingPackageName(),
+                            targetPackage.getPackageName())) {
                 return true;
             }
         } catch (PackageManager.NameNotFoundException e) {
@@ -232,8 +238,8 @@
      * certificate was once used to sign the package, the package will still be granted access. This
      * does not handle packages that have been signed by multiple certificates.
      */
-    private boolean isSchemaVisibleToPackages(@NonNull SchemaVisibilityConfig visibilityConfig,
-            int callerUid) {
+    private boolean isSchemaVisibleToPackages(
+            @NonNull SchemaVisibilityConfig visibilityConfig, int callerUid) {
         List<PackageIdentifier> visibleToPackages = visibilityConfig.getAllowedPackages();
         for (int i = 0; i < visibleToPackages.size(); i++) {
             PackageIdentifier visibleToPackage = visibleToPackages.get(i);
@@ -247,8 +253,8 @@
             // make calls to us. So just check if the appId portion of the uid is the same. This is
             // essentially UserHandle.isSameApp, but that's not a system API for us to use.
             int callerAppId = UserHandle.getAppId(callerUid);
-            int packageUid = PackageUtil.getPackageUid(
-                    mUserContext, visibleToPackage.getPackageName());
+            int packageUid =
+                    PackageUtil.getPackageUid(mUserContext, visibleToPackage.getPackageName());
             int userAppId = UserHandle.getAppId(packageUid);
             if (callerAppId != userAppId) {
                 continue;
@@ -267,9 +273,7 @@
         return false;
     }
 
-    /**
-     * Returns whether the caller holds required permissions for the given schema.
-     */
+    /** Returns whether the caller holds required permissions for the given schema. */
     private boolean isSchemaVisibleToPermission(
             @NonNull SchemaVisibilityConfig visibilityConfig,
             @Nullable AppSearchAttributionSource callerAttributionSource,
@@ -283,8 +287,8 @@
         for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
             // User may set multiple required permission sets. Provider need to hold ALL required
             // permission of ANY of the individual value sets.
-            if (doesCallerHoldsAllRequiredPermissions(allRequiredPermissions,
-                    callerAttributionSource, checkEnterpriseAccess)) {
+            if (doesCallerHoldsAllRequiredPermissions(
+                    allRequiredPermissions, callerAttributionSource, checkEnterpriseAccess)) {
                 // The calling package has all required permissions in this set, return true.
                 return true;
             }
@@ -300,8 +304,8 @@
             boolean checkEnterpriseAccess) {
         // A permissions set with ENTERPRISE_ACCESS should only be checked by enterprise calls,
         // and enterprise calls should only check permission sets with ENTERPRISE_ACCESS
-        boolean isEnterprisePermissionsSet = allRequiredPermissions.contains(
-                SetSchemaRequest.ENTERPRISE_ACCESS);
+        boolean isEnterprisePermissionsSet =
+                allRequiredPermissions.contains(SetSchemaRequest.ENTERPRISE_ACCESS);
         if (checkEnterpriseAccess != isEnterprisePermissionsSet) {
             return false;
         }
@@ -314,8 +318,8 @@
                 case SetSchemaRequest.READ_EXTERNAL_STORAGE:
                 case SetSchemaRequest.READ_HOME_APP_SEARCH_DATA:
                 case SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA:
-                    if (!doesCallerHavePermissionForDataDelivery(requiredPermission,
-                            callerAttributionSource)) {
+                    if (!doesCallerHavePermissionForDataDelivery(
+                            requiredPermission, callerAttributionSource)) {
                         // The calling package doesn't have this required permission, return false.
                         return false;
                     }
@@ -328,7 +332,7 @@
                     callingPackageName = callerAttributionSource.getPackageName();
                     if (callingPackageName == null
                             || !mPolicyChecker.doesCallerHaveManagedProfileContactsAccess(
-                            callingPackageName)) {
+                                    callingPackageName)) {
                         return false;
                     }
                     break;
@@ -378,9 +382,11 @@
         }
         // getAttributionSource can be safely called and the returned value will only be
         // null on Android R-
-        return PERMISSION_GRANTED == mPermissionManager.checkPermissionForDataDelivery(
-                permission, callerAttributionSource.getAttributionSource(),
-                /*message=*/"appsearch");
+        return PERMISSION_GRANTED
+                == mPermissionManager.checkPermissionForDataDelivery(
+                        permission,
+                        callerAttributionSource.getAttributionSource(),
+                        /* message= */ "appsearch");
     }
 
     /**
@@ -391,8 +397,9 @@
     @Override
     public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
         Objects.requireNonNull(callerPackageName);
-        return mUserContext.getPackageManager()
-                .checkPermission(READ_GLOBAL_APP_SEARCH_DATA, callerPackageName)
+        return mUserContext
+                        .getPackageManager()
+                        .checkPermission(READ_GLOBAL_APP_SEARCH_DATA, callerPackageName)
                 == PackageManager.PERMISSION_GRANTED;
     }
 }
diff --git a/synced_jetpack_sha.txt b/synced_jetpack_sha.txt
index 8ca43bb..dfdbf1a 100644
--- a/synced_jetpack_sha.txt
+++ b/synced_jetpack_sha.txt
@@ -1 +1 @@
-10d4bc0f4d79b7e0801030054bacd3658d588841
+f217c908e6d0a8529bf287a145b20ebd77403eb8
diff --git a/testing/appsindexertests/Android.bp b/testing/appsindexertests/Android.bp
new file mode 100644
index 0000000..fb47c20
--- /dev/null
+++ b/testing/appsindexertests/Android.bp
@@ -0,0 +1,45 @@
+// Copyright (C) 2024 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 {
+    default_team: "trendy_team_appsearch",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AppsIndexerTests",
+    srcs: ["src/**/*.java"],
+    defaults: ["modules-utils-testable-device-config-defaults"],
+    static_libs: [
+        "CtsAppSearchTestUtils",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "compatibility-device-util-axt",
+        "service-appsearch-for-tests",
+        "services.core",
+        "truth",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.mock",
+        "android.test.base",
+        "framework-appsearch.impl",
+    ],
+    test_suites: [
+        "general-tests",
+        "mts-appsearch",
+    ],
+    compile_multilib: "both",
+    min_sdk_version: "33",
+}
diff --git a/testing/appsindexertests/AndroidManifest.xml b/testing/appsindexertests/AndroidManifest.xml
new file mode 100644
index 0000000..cb0ec9a
--- /dev/null
+++ b/testing/appsindexertests/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.appsearch.appsindexertests" >
+    <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <application android:label="AppsIndexerTests"
+                 android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+        <service android:name="com.android.server.appsearch.appsindexer.IndexerMaintenanceService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+        <service android:name="com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.appsearch.appsindexertests"
+                     android:label="AppsIndexerTests"/>
+</manifest>
diff --git a/testing/appsindexertests/AndroidTest.xml b/testing/appsindexertests/AndroidTest.xml
new file mode 100644
index 0000000..8f9f9e8
--- /dev/null
+++ b/testing/appsindexertests/AndroidTest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+<configuration description="Runs Apps Indexer tests">
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="AppsIndexerTests.apk"/>
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.appsearch.appsindexertests"/>
+        <option name="exclude-annotation"
+            value="com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile" />
+        <option name="exclude-annotation"
+            value="com.android.bedstead.harrier.annotations.RequireRunOnSecondaryUser" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.appsearch" />
+    </object>
+
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+</configuration>
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppSearchHelperTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppSearchHelperTest.java
new file mode 100644
index 0000000..2db9730
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppSearchHelperTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static com.android.server.appsearch.appsindexer.TestUtils.COMPATIBLE_APP_SCHEMA;
+import static com.android.server.appsearch.appsindexer.TestUtils.FAKE_PACKAGE_PREFIX;
+import static com.android.server.appsearch.appsindexer.TestUtils.FAKE_SIGNATURE;
+import static com.android.server.appsearch.appsindexer.TestUtils.INCOMPATIBLE_APP_SCHEMA;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeAppIndexerSession;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeMobileApplication;
+import static com.android.server.appsearch.appsindexer.TestUtils.createMobileApplications;
+import static com.android.server.appsearch.appsindexer.TestUtils.createMockPackageIdentifier;
+import static com.android.server.appsearch.appsindexer.TestUtils.createMockPackageIdentifiers;
+import static com.android.server.appsearch.appsindexer.TestUtils.removeFakePackageDocuments;
+import static com.android.server.appsearch.appsindexer.TestUtils.searchAppSearchForApps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GetSchemaResponse;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Since AppSearchHelper mainly just calls AppSearch's api to index/remove files, we shouldn't worry
+ * too much about it since AppSearch has good test coverage. Here just add some simple checks.
+ */
+public class AppSearchHelperTest {
+    private final ExecutorService mSingleThreadedExecutor = Executors.newSingleThreadExecutor();
+    private Context mContext;
+    private AppSearchHelper mAppSearchHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mAppSearchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        removeFakePackageDocuments(mContext, mSingleThreadedExecutor);
+        mAppSearchHelper.close();
+    }
+
+    @Test
+    public void testAppSearchHelper_permissionSetCorrectlyForMobileApplication() throws Exception {
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(1));
+        mAppSearchHelper.indexApps(createMobileApplications(1));
+
+        AppSearchSessionShim session =
+                createFakeAppIndexerSession(mContext, mSingleThreadedExecutor);
+        GetSchemaResponse response = session.getSchemaAsync().get();
+
+        assertThat(response.getSchemas())
+                .contains(
+                        MobileApplication.createMobileApplicationSchemaForPackage(
+                                "com.fake.package0"));
+        PackageIdentifier expected =
+                new PackageIdentifier("com.fake.package0", FAKE_SIGNATURE.toByteArray());
+        assertThat(response.getPubliclyVisibleSchemas().keySet())
+                .containsExactly(MobileApplication.SCHEMA_TYPE + "-" + FAKE_PACKAGE_PREFIX + "0");
+        PackageIdentifier actual =
+                response.getPubliclyVisibleSchemas().values().toArray(new PackageIdentifier[0])[0];
+        assertThat(actual.getSha256Certificate()).isEqualTo(expected.getSha256Certificate());
+        assertThat(actual.getPackageName()).isEqualTo(expected.getPackageName());
+    }
+
+    @Test
+    public void testIndexManyApps() throws Exception {
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(600));
+        mAppSearchHelper.indexApps(createMobileApplications(600));
+        Map<String, Long> appsearchIds = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(appsearchIds.size()).isEqualTo(600);
+        List<SearchResult> real = searchAppSearchForApps(600 + 1);
+        assertThat(real).hasSize(600);
+        removeFakePackageDocuments(mContext, mSingleThreadedExecutor);
+    }
+
+    @Test
+    public void testIndexApps_compatibleSchemaChange() throws Exception {
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(COMPATIBLE_APP_SCHEMA)
+                        .setForceOverride(true)
+                        .build();
+
+        int variant = 0;
+        AppSearchSessionShim session =
+                createFakeAppIndexerSession(mContext, mSingleThreadedExecutor);
+        session.setSchemaAsync(setSchemaRequest).get();
+
+        AppSearchHelper appSearchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+        appSearchHelper.setSchemasForPackages(
+                ImmutableList.of(createMockPackageIdentifier(variant)));
+        appSearchHelper.indexApps(ImmutableList.of(createFakeMobileApplication(variant)));
+
+        assertThat(appSearchHelper).isNotNull();
+        List<SearchResult> results = searchAppSearchForApps(1 + 1);
+        assertThat(results).hasSize(1);
+        assertThat(results.get(0).getGenericDocument().getId()).isEqualTo("com.fake.package0");
+    }
+
+    @Test
+    public void testIndexApps_incompatibleSchemaChange() throws Exception {
+        AppSearchSessionShim session =
+                createFakeAppIndexerSession(mContext, mSingleThreadedExecutor);
+
+        // Set incompatible schemas that would be removed
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(INCOMPATIBLE_APP_SCHEMA)
+                        .setForceOverride(true)
+                        .build();
+        session.setSchemaAsync(setSchemaRequest).get();
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(50));
+        mAppSearchHelper.indexApps(createMobileApplications(50));
+
+        List<SearchResult> real = searchAppSearchForApps(50 + 1);
+        assertThat(real).hasSize(50);
+    }
+
+    @Test
+    public void testIndexApps_outOfSpace_shouldNotCompleteNormally() throws Exception {
+        // set up AppSearchSession#put to invoke the callback with a RESULT_OUT_OF_SPACE failure
+        SyncAppSearchSession fullSession = Mockito.mock(SyncAppSearchSession.class);
+        when(fullSession.put(any(PutDocumentsRequest.class)))
+                .thenReturn(
+                        new AppSearchBatchResult.Builder<String, Void>()
+                                .setFailure(
+                                        "id", AppSearchResult.RESULT_OUT_OF_SPACE, "errorMessage")
+                                .build());
+        AppSearchHelper mocked = AppSearchHelper.createAppSearchHelper(mContext);
+        mocked.setAppSearchSession(fullSession);
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(1));
+        // It should throw if it's out of space
+        assertThrows(
+                AppSearchException.class,
+                () -> mocked.indexApps(ImmutableList.of(createFakeMobileApplication(0))));
+    }
+
+    @Test
+    public void testAppSearchHelper_removeApps() throws Exception {
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(100));
+        mAppSearchHelper.indexApps(createMobileApplications(100));
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(50));
+
+        List<String> deletedIds = new ArrayList<>();
+        // Last 50 ids should be removed.
+        for (int i = 50; i < 100; i++) {
+            deletedIds.add(FAKE_PACKAGE_PREFIX + i);
+        }
+
+        Map<String, Long> indexedIds = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(indexedIds.size()).isEqualTo(50);
+        Map<String, Long> appsearchIds = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(appsearchIds.keySet()).containsNoneIn(deletedIds);
+    }
+
+    @Test
+    public void test_sameApp_notIndexed() throws Exception {
+        MobileApplication app0 = createFakeMobileApplication(0);
+        MobileApplication app1 = createFakeMobileApplication(1);
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(2));
+        mAppSearchHelper.indexApps(ImmutableList.of(app0, app1));
+        Map<String, Long> timestampMapping = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(timestampMapping)
+                .containsExactly("com.fake.package0", 0L, "com.fake.package1", 1L);
+
+        // Try to add the same apps
+        mAppSearchHelper.indexApps(ImmutableList.of(app0, app1));
+
+        // Should still be two
+        timestampMapping = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(timestampMapping)
+                .containsExactly("com.fake.package0", 0L, "com.fake.package1", 1L);
+    }
+
+    @Test
+    public void test_appDifferent_reIndexed() throws Exception {
+        MobileApplication app0 = createFakeMobileApplication(0);
+        MobileApplication app1 = createFakeMobileApplication(1);
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(2));
+        mAppSearchHelper.indexApps(ImmutableList.of(app0, app1));
+        Map<String, Long> timestampMapping = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(timestampMapping)
+                .containsExactly("com.fake.package0", 0L, "com.fake.package1", 1L);
+
+        // Check what happens if we keep the same id
+        app1 =
+                new MobileApplication.Builder(FAKE_PACKAGE_PREFIX + 1, FAKE_SIGNATURE.toByteArray())
+                        .setDisplayName("Fake Application Name")
+                        .setIconUri("https://cs.android.com")
+                        .setClassName(".class")
+                        .setUpdatedTimestampMs(300)
+                        .setAlternateNames("Joe")
+                        .build();
+
+        // Should update the app, not add a new one
+        mAppSearchHelper.indexApps(ImmutableList.of(app1));
+        timestampMapping = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(timestampMapping)
+                .containsExactly("com.fake.package0", 0L, "com.fake.package1", 300L);
+    }
+
+    @Test
+    public void test_appNew_indexed() throws Exception {
+        MobileApplication app0 = createFakeMobileApplication(0);
+        MobileApplication app1 = createFakeMobileApplication(1);
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(2));
+        mAppSearchHelper.indexApps(ImmutableList.of(app0, app1));
+        assertThat(mAppSearchHelper.getAppsFromAppSearch()).hasSize(2);
+
+        MobileApplication app2 = createFakeMobileApplication(2);
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(3));
+        mAppSearchHelper.indexApps(ImmutableList.of(app0, app1, app2));
+
+        // Should be three
+        Map<String, Long> timestampMapping = mAppSearchHelper.getAppsFromAppSearch();
+        assertThat(timestampMapping)
+                .containsExactly(
+                        "com.fake.package0", 0L, "com.fake.package1", 1L, "com.fake.package2", 2L);
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerImplTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerImplTest.java
new file mode 100644
index 0000000..20c4d05
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerImplTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeMobileApplication;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakePackageInfos;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeResolveInfos;
+import static com.android.server.appsearch.appsindexer.TestUtils.createMockPackageIdentifiers;
+import static com.android.server.appsearch.appsindexer.TestUtils.removeFakePackageDocuments;
+import static com.android.server.appsearch.appsindexer.TestUtils.setupMockPackageManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class AppsIndexerImplTest {
+    private AppSearchHelper mAppSearchHelper;
+    private Context mContext;
+    @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+    private final ExecutorService mSingleThreadedExecutor = Executors.newSingleThreadExecutor();
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = ApplicationProvider.getApplicationContext();
+        mAppSearchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        removeFakePackageDocuments(mContext, mSingleThreadedExecutor);
+        mAppSearchHelper.close();
+    }
+
+    @Test
+    public void testAppsIndexerImpl_removeApps() throws Exception {
+        // Add some apps
+        MobileApplication app1 = createFakeMobileApplication(0);
+        MobileApplication app2 = createFakeMobileApplication(1);
+
+        mAppSearchHelper.setSchemasForPackages(createMockPackageIdentifiers(2));
+        mAppSearchHelper.indexApps(ImmutableList.of(app1, app2));
+        Map<String, Long> appTimestampMap = mAppSearchHelper.getAppsFromAppSearch();
+
+        List<String> packageIds = new ArrayList<>(appTimestampMap.keySet());
+        assertThat(packageIds).containsExactly("com.fake.package0", "com.fake.package1");
+
+        // Set up mock so that just 1 document is returned, as if we deleted a doc
+        PackageManager pm = Mockito.mock(PackageManager.class);
+        setupMockPackageManager(pm, createFakePackageInfos(1), createFakeResolveInfos(1));
+        Context context =
+                new ContextWrapper(mContext) {
+                    @Override
+                    public PackageManager getPackageManager() {
+                        return pm;
+                    }
+                };
+        try (AppsIndexerImpl appsIndexerImpl = new AppsIndexerImpl(context)) {
+            appsIndexerImpl.doUpdate(new AppsIndexerSettings(temporaryFolder.newFolder("temp")));
+
+            assertThat(mAppSearchHelper.getAppsFromAppSearch().keySet())
+                    .containsExactly("com.fake.package0");
+        }
+    }
+
+    @Test
+    public void testAppsIndexerImpl_updateAppsThrowsError_shouldContinueOnError() throws Exception {
+        PackageManager pm = Mockito.mock(PackageManager.class);
+        when(pm.getInstalledPackages(any())).thenThrow(new RuntimeException("fake"));
+        Context context =
+                new ContextWrapper(mContext) {
+                    @Override
+                    public PackageManager getPackageManager() {
+                        return pm;
+                    }
+                };
+        try (AppsIndexerImpl appsIndexerImpl = new AppsIndexerImpl(context)) {
+            appsIndexerImpl.doUpdate(new AppsIndexerSettings(temporaryFolder.newFolder("tmp")));
+
+            // Shouldn't throw, but no apps indexed
+            assertThat(mAppSearchHelper.getAppsFromAppSearch()).isEmpty();
+        }
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerMaintenanceTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerMaintenanceTest.java
new file mode 100644
index 0000000..b74a0c0
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerMaintenanceTest.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static android.Manifest.permission.RECEIVE_BOOT_COMPLETED;
+
+import static com.android.server.appsearch.appsindexer.AppsIndexerMaintenanceConfig.MIN_APPS_INDEXER_JOB_ID;
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.APPS_INDEXER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.UserIdInt;
+import android.app.UiAutomation;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.UserInfo;
+import android.os.CancellationSignal;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.SystemService;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class AppsIndexerMaintenanceTest {
+    private static final int DEFAULT_USER_ID = 0;
+    private static final UserHandle DEFAULT_USER_HANDLE = new UserHandle(DEFAULT_USER_ID);
+
+    private Context mContext = ApplicationProvider.getApplicationContext();
+    private Context mContextWrapper;
+    private IndexerMaintenanceService mAppsIndexerMaintenanceService;
+    private MockitoSession mSession;
+    @Mock private JobScheduler mMockJobScheduler;
+    private JobParameters mParams;
+    private PersistableBundle mExtras;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContextWrapper =
+                new ContextWrapper(mContext) {
+                    @Override
+                    @Nullable
+                    public Object getSystemService(String name) {
+                        if (Context.JOB_SCHEDULER_SERVICE.equals(name)) {
+                            return mMockJobScheduler;
+                        }
+                        return getSystemService(name);
+                    }
+                };
+        mAppsIndexerMaintenanceService = spy(new IndexerMaintenanceService());
+        doNothing().when(mAppsIndexerMaintenanceService).jobFinished(any(), anyBoolean());
+        mSession =
+                ExtendedMockito.mockitoSession()
+                        .mockStatic(LocalManagerRegistry.class)
+                        .startMocking();
+        mExtras = new PersistableBundle();
+        mExtras.putInt("indexer_type", APPS_INDEXER);
+        mParams = Mockito.mock(JobParameters.class);
+    }
+
+    @After
+    public void tearDown() {
+        mSession.finishMocking();
+        mAppsIndexerMaintenanceService.destroy();
+    }
+
+    @Test
+    public void testScheduleUpdateJob_oneOff_isNotPeriodic() {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+            IndexerMaintenanceService.scheduleUpdateJob(
+                    mContext,
+                    DEFAULT_USER_HANDLE,
+                    APPS_INDEXER,
+                    /* periodic= */ false,
+                    /* intervalMillis= */ -1);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        JobInfo jobInfo = getPendingUpdateJob(DEFAULT_USER_ID);
+        assertThat(jobInfo).isNotNull();
+        assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+        assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+        assertThat(jobInfo.isPersisted()).isTrue();
+        assertThat(jobInfo.isPeriodic()).isFalse();
+    }
+
+    @Test
+    public void testScheduleUpdateJob_periodic_isPeriodic() {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+            IndexerMaintenanceService.scheduleUpdateJob(
+                    mContext,
+                    /* userId= */ DEFAULT_USER_HANDLE,
+                    /* indexerType= */ APPS_INDEXER,
+                    /* periodic= */ true,
+                    /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        JobInfo jobInfo = getPendingUpdateJob(DEFAULT_USER_ID);
+        assertThat(jobInfo).isNotNull();
+        assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+        assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+        assertThat(jobInfo.isPersisted()).isTrue();
+        assertThat(jobInfo.isPeriodic()).isTrue();
+        assertThat(jobInfo.getIntervalMillis()).isEqualTo(TimeUnit.DAYS.toMillis(7));
+        assertThat(jobInfo.getFlexMillis()).isEqualTo(TimeUnit.DAYS.toMillis(7) / 2);
+    }
+
+    @Test
+    public void testScheduleUpdateJob_oneOffThenPeriodic_isRescheduled() {
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ APPS_INDEXER,
+                /* periodic= */ false,
+                /* intervalMillis= */ -1);
+        ArgumentCaptor<JobInfo> firstJobInfoCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mMockJobScheduler).schedule(firstJobInfoCaptor.capture());
+        JobInfo firstJobInfo = firstJobInfoCaptor.getValue();
+
+        when(mMockJobScheduler.getPendingJob(eq(MIN_APPS_INDEXER_JOB_ID))).thenReturn(firstJobInfo);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ APPS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
+        ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mMockJobScheduler, times(2)).schedule(argumentCaptor.capture());
+        List<JobInfo> jobInfos = argumentCaptor.getAllValues();
+        JobInfo jobInfo = jobInfos.get(1);
+        assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+        assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+        assertThat(jobInfo.isPersisted()).isTrue();
+        assertThat(jobInfo.isPeriodic()).isTrue();
+        assertThat(jobInfo.getIntervalMillis()).isEqualTo(TimeUnit.DAYS.toMillis(7));
+    }
+
+    @Test
+    public void testScheduleUpdateJob_differentParams_isRescheduled() {
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ APPS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
+        ArgumentCaptor<JobInfo> firstJobInfoCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mMockJobScheduler).schedule(firstJobInfoCaptor.capture());
+        JobInfo firstJobInfo = firstJobInfoCaptor.getValue();
+
+        when(mMockJobScheduler.getPendingJob(eq(MIN_APPS_INDEXER_JOB_ID))).thenReturn(firstJobInfo);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ APPS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(30));
+        ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        // Mockito.verify() counts the number of occurrences from the beginning of the test.
+        // This verify() uses times(2) to also account for the call to JobScheduler.schedule() above
+        // where the first JobInfo is captured.
+        verify(mMockJobScheduler, times(2)).schedule(argumentCaptor.capture());
+        List<JobInfo> jobInfos = argumentCaptor.getAllValues();
+        JobInfo jobInfo = jobInfos.get(1);
+        assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+        assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+        assertThat(jobInfo.isPersisted()).isTrue();
+        assertThat(jobInfo.isPeriodic()).isTrue();
+        assertThat(jobInfo.getIntervalMillis()).isEqualTo(TimeUnit.DAYS.toMillis(30));
+    }
+
+    @Test
+    public void testScheduleUpdateJob_sameParams_isNotRescheduled() {
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ APPS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
+        ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mMockJobScheduler).schedule(argumentCaptor.capture());
+        JobInfo firstJobInfo = argumentCaptor.getValue();
+
+        when(mMockJobScheduler.getPendingJob(eq(MIN_APPS_INDEXER_JOB_ID))).thenReturn(firstJobInfo);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ APPS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
+        // Mockito.verify() counts the number of occurrences from the beginning of the test.
+        // This verify() uses the default count of 1 (equivalent to times(1)) to account for the
+        // call to JobScheduler.schedule() above where the first JobInfo is captured.
+        verify(mMockJobScheduler).schedule(any(JobInfo.class));
+    }
+
+    @Test
+    public void testDoUpdateForUser_withInitializedLocalService_isSuccessful() {
+        when(mParams.getExtras()).thenReturn(mExtras);
+        ExtendedMockito.doReturn(Mockito.mock(AppsIndexerManagerService.LocalService.class))
+                .when(
+                        () ->
+                                LocalManagerRegistry.getManager(
+                                        AppsIndexerManagerService.LocalService.class));
+        boolean updateSucceeded =
+                mAppsIndexerMaintenanceService.doUpdateForUser(
+                        mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
+        assertThat(updateSucceeded).isTrue();
+    }
+
+    @Test
+    public void testDoUpdateForUser_withUninitializedLocalService_failsGracefully() {
+        when(mParams.getExtras()).thenReturn(mExtras);
+        ExtendedMockito.doReturn(null)
+                .when(
+                        () ->
+                                LocalManagerRegistry.getManager(
+                                        AppsIndexerManagerService.LocalService.class));
+        boolean updateSucceeded =
+                mAppsIndexerMaintenanceService.doUpdateForUser(
+                        mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
+        assertThat(updateSucceeded).isFalse();
+    }
+
+    @Test
+    public void testDoUpdateForUser_onEncounteringException_failsGracefully() {
+        when(mParams.getExtras()).thenReturn(mExtras);
+        AppsIndexerManagerService.LocalService mockService =
+                Mockito.mock(AppsIndexerManagerService.LocalService.class);
+        doThrow(RuntimeException.class)
+                .when(mockService)
+                .doUpdateForUser((UserHandle) any(), (CancellationSignal) any());
+        ExtendedMockito.doReturn(mockService)
+                .when(
+                        () ->
+                                LocalManagerRegistry.getManager(
+                                        AppsIndexerManagerService.LocalService.class));
+
+        boolean updateSucceeded =
+                mAppsIndexerMaintenanceService.doUpdateForUser(
+                        mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
+
+        assertThat(updateSucceeded).isFalse();
+    }
+
+    @Test
+    public void testDoUpdateForUser_cancelsBackgroundJob_whenIndexerDisabled() {
+        when(mParams.getExtras()).thenReturn(mExtras);
+        ExtendedMockito.doReturn(null)
+                .when(
+                        () ->
+                                LocalManagerRegistry.getManager(
+                                        AppsIndexerManagerService.LocalService.class));
+
+        mAppsIndexerMaintenanceService.doUpdateForUser(
+                mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
+
+        verify(mMockJobScheduler).cancel(MIN_APPS_INDEXER_JOB_ID);
+    }
+
+    @Test
+    public void testDoUpdateForUser_doesNotCancelBackgroundJob_whenIndexerEnabled() {
+        when(mParams.getExtras()).thenReturn(mExtras);
+        ExtendedMockito.doReturn(Mockito.mock(AppsIndexerManagerService.LocalService.class))
+                .when(
+                        () ->
+                                LocalManagerRegistry.getManager(
+                                        AppsIndexerManagerService.LocalService.class));
+
+        mAppsIndexerMaintenanceService.doUpdateForUser(
+                mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
+
+        verifyZeroInteractions(mMockJobScheduler);
+    }
+
+    @Test
+    public void testCancelPendingUpdateJob_succeeds() throws IOException {
+        UserInfo userInfo = new UserInfo(DEFAULT_USER_ID, /* name= */ "default", /* flags= */ 0);
+        SystemService.TargetUser user = new SystemService.TargetUser(userInfo);
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+            IndexerMaintenanceService.scheduleUpdateJob(
+                    mContext,
+                    DEFAULT_USER_HANDLE,
+                    /* indexerType= */ APPS_INDEXER,
+                    /* periodic= */ true,
+                    /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        JobInfo jobInfo = getPendingUpdateJob(DEFAULT_USER_ID);
+        assertThat(jobInfo).isNotNull();
+
+        IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                mContext, user.getUserHandle(), APPS_INDEXER);
+
+        jobInfo = getPendingUpdateJob(DEFAULT_USER_ID);
+        assertThat(jobInfo).isNull();
+    }
+
+    @Test
+    public void test_onStartJob_handlesExceptionGracefully() {
+        mAppsIndexerMaintenanceService.onStartJob(mParams);
+    }
+
+    @Test
+    public void test_onStopJob_handlesExceptionGracefully() {
+        mAppsIndexerMaintenanceService.onStopJob(mParams);
+    }
+
+    @Nullable
+    private JobInfo getPendingUpdateJob(@UserIdInt int userId) {
+        int jobId = MIN_APPS_INDEXER_JOB_ID + userId;
+        return mContext.getSystemService(JobScheduler.class).getPendingJob(jobId);
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerManagerServiceTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerManagerServiceTest.java
new file mode 100644
index 0000000..cf3f247
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerManagerServiceTest.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.RECEIVE_BOOT_COMPLETED;
+
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeAppIndexerSession;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakePackageInfo;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakePackageInfos;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeResolveInfo;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeResolveInfos;
+import static com.android.server.appsearch.appsindexer.TestUtils.setupMockPackageManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.app.UiAutomation;
+import android.app.appsearch.AppSearchEnvironmentFactory;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.FrameworkAppSearchEnvironment;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.SystemService;
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class AppsIndexerManagerServiceTest extends AppsIndexerTestBase {
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private final ExecutorService mSingleThreadedExecutor = Executors.newSingleThreadExecutor();
+    private AppsIndexerManagerService mAppsIndexerManagerService;
+    private UiAutomation mUiAutomation;
+    private BroadcastReceiver mCapturedReceiver;
+    // Saving to class so we can unregister the callback
+    private final PackageManager mPackageManager = Mockito.mock(PackageManager.class);
+    private GlobalSearchSessionShim mShim;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        Context context = ApplicationProvider.getApplicationContext();
+        mContext =
+                new ContextWrapper(context) {
+                    @Override
+                    public Context createContextAsUser(UserHandle user, int flags) {
+                        return new ContextWrapper(super.createContextAsUser(user, flags)) {
+                            @Override
+                            public PackageManager getPackageManager() {
+                                return mPackageManager;
+                            }
+                        };
+                    }
+
+                    @Nullable
+                    @Override
+                    public Intent registerReceiverForAllUsers(
+                            @Nullable BroadcastReceiver receiver,
+                            @NonNull IntentFilter filter,
+                            @Nullable String broadcastPermission,
+                            @Nullable Handler scheduler) {
+                        mCapturedReceiver = receiver;
+                        return super.registerReceiverForAllUsers(
+                                receiver,
+                                filter,
+                                broadcastPermission,
+                                scheduler,
+                                Context.RECEIVER_NOT_EXPORTED);
+                    }
+                };
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        // INTERACT_ACROSS_USERS_FULL: needed when we do registerReceiverForAllUsers for getting
+        // package change notifications.
+        mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL);
+
+        File mAppSearchDir = mTemporaryFolder.newFolder();
+        AppSearchEnvironmentFactory.setEnvironmentInstanceForTest(
+                new FrameworkAppSearchEnvironment() {
+                    @Override
+                    public File getAppSearchDir(
+                            @NonNull Context unused, @NonNull UserHandle userHandle) {
+                        return mAppSearchDir;
+                    }
+                });
+
+        mAppsIndexerManagerService =
+                new AppsIndexerManagerService(mContext, new TestAppsIndexerConfig());
+        try {
+            mAppsIndexerManagerService.onStart();
+        } catch (Exception e) {
+            // This might fail due to LocalService already being registered. Ignore it for the test
+        }
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        // Wipe the data in AppSearchHelper.DATABASE_NAME.
+        AppSearchSessionShim db = createFakeAppIndexerSession(mContext, mSingleThreadedExecutor);
+
+        db.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+
+        mUiAutomation.dropShellPermissionIdentity();
+        super.tearDown();
+    }
+
+    @Test
+    public void testBootstrapPackages() throws Exception {
+        // Populate fake PackageManager with fake Packages.
+        int numFakePackages = 3;
+        List<PackageInfo> fakePackages = new ArrayList<>(createFakePackageInfos(numFakePackages));
+        List<ResolveInfo> fakeActivities = new ArrayList<>(createFakeResolveInfos(numFakePackages));
+
+        setupMockPackageManager(mPackageManager, fakePackages, fakeActivities);
+
+        UserInfo userInfo =
+                new UserInfo(
+                        mContext.getUser().getIdentifier(), /* name= */ "default", /* flags= */ 0);
+        GlobalSearchSessionShim db =
+                GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
+        // Apps indexer schedules a full-update job for bootstrapping from PackageManager,
+        // and JobScheduler API requires BOOT_COMPLETED permission for persisting the job.
+        mUiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+        try {
+            CountDownLatch bootstrapLatch =
+                    setupLatch(numFakePackages, /* listenForSchemaChanges= */ false);
+            mAppsIndexerManagerService.onUserUnlocking(new SystemService.TargetUser(userInfo));
+            assertTrue(bootstrapLatch.await(10000L, TimeUnit.MILLISECONDS));
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+
+        // Ensure that we can query the package documents added to AppSearch
+        SearchResultsShim results =
+                db.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setRankingStrategy("this.creationTimestamp()")
+                                .addFilterNamespaces(MobileApplication.APPS_NAMESPACE)
+                                .addFilterPackageNames(mContext.getPackageName())
+                                .build());
+
+        List<SearchResult> page = results.getNextPageAsync().get();
+        assertThat(page).hasSize(numFakePackages);
+        List<String> schemaNames = new ArrayList<>();
+        for (int i = 0; i < page.size(); i++) {
+            schemaNames.add(page.get(i).getGenericDocument().getSchemaType());
+        }
+        assertThat(schemaNames)
+                .containsExactly(
+                        "builtin:MobileApplication-com.fake.package2",
+                        "builtin:MobileApplication-com.fake.package1",
+                        "builtin:MobileApplication-com.fake.package0");
+
+        mAppsIndexerManagerService.onUserStopping(new SystemService.TargetUser(userInfo));
+    }
+
+    @Test
+    public void testAddPackage() throws Exception {
+        // Populate fake PackageManager with fake Packages.
+        int numFakePackages = 3;
+        List<PackageInfo> fakePackages = new ArrayList<>(createFakePackageInfos(numFakePackages));
+        List<ResolveInfo> fakeActivities = new ArrayList<>(createFakeResolveInfos(numFakePackages));
+
+        setupMockPackageManager(mPackageManager, fakePackages, fakeActivities);
+
+        UserInfo userInfo =
+                new UserInfo(
+                        mContext.getUser().getIdentifier(), /* name= */ "default", /* flags= */ 0);
+        GlobalSearchSessionShim db =
+                GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
+        // Apps indexer schedules a full-update job for bootstrapping from PackageManager,
+        // and JobScheduler API requires BOOT_COMPLETED permission for persisting the job.
+        mUiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+        CountDownLatch bootstrapLatch = null;
+        try {
+            bootstrapLatch = setupLatch(numFakePackages, /* listenForSchemaChanges= */ false);
+            mAppsIndexerManagerService.onUserUnlocking(new SystemService.TargetUser(userInfo));
+            assertTrue(bootstrapLatch.await(10000L, TimeUnit.MILLISECONDS));
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+
+        // Add a package and trigger an update directly
+        Intent fakeIntent = new Intent(Intent.ACTION_PACKAGE_ADDED);
+        fakeIntent.setData(Uri.parse("package:" + mContext.getPackageName()));
+        fakeIntent.putExtra(Intent.EXTRA_UID, userInfo.id);
+
+        // Add a package at index numFakePackages
+        fakePackages.add(createFakePackageInfo(numFakePackages));
+        fakeActivities.add(createFakeResolveInfo(numFakePackages));
+        CountDownLatch latch = setupLatch(1, /* listenForSchemaChanges= */ false);
+
+        mCapturedReceiver.onReceive(mContext, fakeIntent);
+        assertTrue(latch.await(10000L, TimeUnit.MILLISECONDS));
+
+        // Wait for the change then Check AppSearch
+        SearchResultsShim results =
+                db.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .addFilterPackageNames(mContext.getPackageName())
+                                .setResultCountPerPage(10)
+                                .build());
+        List<SearchResult> page = results.getNextPageAsync().get();
+        // 10 is greater than the expected number of results, which is numFakePackage + 1 = 4
+        assertThat(page).hasSize(numFakePackages + 1);
+
+        mAppsIndexerManagerService.onUserStopping(new SystemService.TargetUser(userInfo));
+    }
+
+    @Test
+    public void testUpdatePackage() throws Exception {
+        // Populate fake PackageManager with fake Packages.
+        int numFakePackages = 3;
+        List<PackageInfo> fakePackages = new ArrayList<>(createFakePackageInfos(numFakePackages));
+        List<ResolveInfo> fakeActivities = new ArrayList<>(createFakeResolveInfos(numFakePackages));
+
+        setupMockPackageManager(mPackageManager, fakePackages, fakeActivities);
+
+        UserInfo userInfo =
+                new UserInfo(
+                        mContext.getUser().getIdentifier(), /* name= */ "default", /* flags= */ 0);
+        GlobalSearchSessionShim db =
+                GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
+        // Apps indexer schedules a full-update job for bootstrapping from PackageManager,
+        // and JobScheduler API requires BOOT_COMPLETED permission for persisting the job.
+        mUiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+        CountDownLatch bootstrapLatch = null;
+        try {
+            bootstrapLatch = setupLatch(numFakePackages, /* listenForSchemaChanges= */ false);
+            mAppsIndexerManagerService.onUserUnlocking(new SystemService.TargetUser(userInfo));
+            assertTrue(bootstrapLatch.await(10000L, TimeUnit.MILLISECONDS));
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+
+        // Update a package by updating the timestamp and trigger an update
+        Intent fakeIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        fakeIntent.setData(Uri.parse("package:" + mContext.getPackageName()));
+        fakeIntent.putExtra(Intent.EXTRA_UID, userInfo.id);
+        // This has to match the package in data to indicate that this was not just a component
+        // change, but that the entire package was changed.
+        fakeIntent.putExtra(
+                Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] {mContext.getPackageName()});
+
+        int updateIndex = 1;
+        fakePackages.get(updateIndex).lastUpdateTime = 1000;
+        CountDownLatch latch = setupLatch(1, /* listenForSchemaChanges= */ false);
+
+        mCapturedReceiver.onReceive(mContext, fakeIntent);
+        assertTrue(latch.await(10000L, TimeUnit.MILLISECONDS));
+
+        // Check AppSearch
+        SearchResultsShim results =
+                db.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .setResultCountPerPage(10)
+                                .addFilterPackageNames(mContext.getPackageName())
+                                .build());
+        List<SearchResult> page = results.getNextPageAsync().get();
+        // 10 is greater than the expected number of results, which is numFakePackage = 3
+        assertThat(page).hasSize(numFakePackages);
+
+        List<Long> timestamps = new ArrayList<>();
+        for (SearchResult result : page) {
+            timestamps.add(
+                    result.getGenericDocument()
+                            .getPropertyLong(MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP));
+        }
+        assertThat(timestamps).contains(1000L);
+
+        mAppsIndexerManagerService.onUserStopping(new SystemService.TargetUser(userInfo));
+    }
+
+    @Test
+    public void testRemovePackage() throws Exception {
+        // Populate fake PackageManager with fake Packages.
+        int numFakePackages = 3;
+        List<PackageInfo> fakePackages = new ArrayList<>(createFakePackageInfos(numFakePackages));
+        List<ResolveInfo> fakeActivities = new ArrayList<>(createFakeResolveInfos(numFakePackages));
+
+        setupMockPackageManager(mPackageManager, fakePackages, fakeActivities);
+
+        UserInfo userInfo =
+                new UserInfo(
+                        mContext.getUser().getIdentifier(), /* name= */ "default", /* flags= */ 0);
+        GlobalSearchSessionShim db =
+                GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
+        // Apps indexer schedules a full-update job for bootstrapping from PackageManager,
+        // and JobScheduler API requires BOOT_COMPLETED permission for persisting the job.
+        mUiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+        CountDownLatch bootstrapLatch = null;
+        try {
+            bootstrapLatch = setupLatch(numFakePackages, /* listenForSchemaChanges= */ false);
+            mAppsIndexerManagerService.onUserUnlocking(new SystemService.TargetUser(userInfo));
+            assertTrue(bootstrapLatch.await(10000L, TimeUnit.MILLISECONDS));
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+
+        // Delete a package and trigger an update
+        Intent fakeIntent = new Intent(Intent.ACTION_PACKAGE_FULLY_REMOVED);
+        fakeIntent.setData(Uri.parse("package:" + mContext.getPackageName()));
+        fakeIntent.putExtra(Intent.EXTRA_UID, userInfo.id);
+
+        fakePackages.remove(0);
+        fakeActivities.remove(0);
+        CountDownLatch latch = setupLatch(1, /* listenForSchemaChanges= */ true);
+
+        mCapturedReceiver.onReceive(mContext, fakeIntent);
+        assertTrue(latch.await(10000L, TimeUnit.MILLISECONDS));
+
+        // Check AppSearch
+        SearchResultsShim results =
+                db.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .addFilterPackageNames(mContext.getPackageName())
+                                .setResultCountPerPage(10)
+                                .build());
+        List<SearchResult> page = results.getNextPageAsync().get();
+        // 10 is greater than the expected number of results, which is numFakePackage - 1 = 2
+        assertThat(page).hasSize(numFakePackages - 1);
+
+        mAppsIndexerManagerService.onUserStopping(new SystemService.TargetUser(userInfo));
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerRealDocumentsTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerRealDocumentsTest.java
new file mode 100644
index 0000000..7f759a7
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerRealDocumentsTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.RECEIVE_BOOT_COMPLETED;
+
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeAppIndexerSession;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.app.appsearch.AppSearchEnvironmentFactory;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.testutil.AppSearchTestUtils;
+import android.content.Intent;
+import android.content.pm.UserInfo;
+import android.net.Uri;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.SystemService;
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class AppsIndexerRealDocumentsTest extends AppsIndexerTestBase {
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        AppSearchSessionShim db =
+                createFakeAppIndexerSession(
+                        ApplicationProvider.getApplicationContext(),
+                        Executors.newSingleThreadExecutor());
+        db.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        db.close();
+        super.tearDown();
+    }
+
+    @Test
+    public void testRealDocuments_check() throws AppSearchException {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
+        assumeTrue(new FrameworkAppsIndexerConfig().isAppsIndexerEnabled());
+        // Ensure that all documents in the android package and with the "apps" namespace are
+        // MobileApplication documents. Read-only test as we are dealing with real apps
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .addFilterPackageNames("android")
+                        .addFilterNamespaces(MobileApplication.APPS_NAMESPACE)
+                        .setResultCountPerPage(100)
+                        .build();
+
+        AppSearchManager manager =
+                ApplicationProvider.getApplicationContext()
+                        .getSystemService(AppSearchManager.class);
+        Executor executor =
+                AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor();
+        SyncGlobalSearchSession globalSearchSession =
+                new SyncGlobalSearchSessionImpl(manager, executor);
+        SyncSearchResults searchResults = globalSearchSession.search("", searchSpec);
+
+        List<SearchResult> results = searchResults.getNextPage();
+
+        // There should be at least settings and other AOSP apps
+        assertThat(results.size()).isGreaterThan(0);
+
+        while (!results.isEmpty()) {
+            for (int i = 0; i < results.size(); i++) {
+                SearchResult result = results.get(i);
+                assertThat(result.getGenericDocument().getSchemaType())
+                        .startsWith(MobileApplication.SCHEMA_TYPE);
+            }
+            results = searchResults.getNextPage();
+        }
+    }
+
+    // Created for system health trace, as close to real as we can get in a test
+    @Test
+    public void testRealIndexing() throws Exception {
+        // Create a real manager service for the test package, no mocking. Use the captured
+        // receiver to simulate package events
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        String testPackage = mContext.getPackageName();
+        UserInfo userInfo =
+                new UserInfo(
+                        mContext.getUser().getIdentifier(), /* name= */ "default", /* flags= */ 0);
+
+        android.os.Trace.beginSection("appIndexer");
+        AppsIndexerManagerService appsIndexerManagerService =
+                new AppsIndexerManagerService(mContext, new TestAppsIndexerConfig());
+
+        uiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL);
+        // This throws an error because the LocalService is already registered. That is fine, we
+        // just need to register the receivers
+        try {
+            appsIndexerManagerService.onStart();
+        } catch (Exception e) {
+        }
+
+        uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+        appsIndexerManagerService.onUserUnlocking(new SystemService.TargetUser(userInfo));
+
+        int userId = new SystemService.TargetUser(userInfo).getUserHandle().getIdentifier();
+        Intent intent = new Intent(Intent.ACTION_PACKAGE_ADDED);
+        intent.setData(Uri.parse("package:" + mContext.getPackageName()));
+        intent.putExtra(Intent.EXTRA_UID, userId);
+        mCapturedReceiver.onReceive(mContext, intent);
+
+        // As the apps get indexed at the same time, we just need to wait for one change.
+        CountDownLatch latch = setupLatch(1, false);
+        assertTrue(latch.await(10L, TimeUnit.SECONDS));
+        mShim.unregisterObserverCallback(testPackage, mCallback);
+
+        SearchResultsShim results =
+                mShim.search(
+                        "",
+                        new SearchSpec.Builder()
+                                .addFilterNamespaces(MobileApplication.APPS_NAMESPACE)
+                                .setResultCountPerPage(50)
+                                .addFilterPackageNames(testPackage, mContext.getPackageName())
+                                .build());
+        List<GenericDocument> documents =
+                AppSearchTestUtils.convertSearchResultsToDocuments(results);
+        assertThat(documents).isNotEmpty();
+        assertThat(documents.get(0).getSchemaType()).startsWith(MobileApplication.SCHEMA_TYPE);
+
+        appsIndexerManagerService.onUserStopping(new SystemService.TargetUser(userInfo));
+
+        android.os.Trace.endSection();
+
+        // Clear it out
+        uiAutomation.dropShellPermissionIdentity();
+        AppSearchSessionShim db =
+                createFakeAppIndexerSession(mContext, Executors.newSingleThreadExecutor());
+        db.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
+        db.close();
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerSettingsTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerSettingsTest.java
new file mode 100644
index 0000000..da1b8df
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerSettingsTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+public class AppsIndexerSettingsTest {
+
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private AppsIndexerSettings mIndexerSettings;
+
+    @Before
+    public void setUp() throws IOException {
+        // Create a test folder for each test
+        File baseDirectory = mTemporaryFolder.newFolder("testAppsIndexerSettings");
+        mIndexerSettings = new AppsIndexerSettings(baseDirectory);
+    }
+
+    @Test
+    public void testLoadAndPersist() throws IOException {
+        // Set some values, persist them, and then load them back
+        mIndexerSettings.setLastUpdateTimestampMillis(123456789L);
+        mIndexerSettings.setLastAppUpdateTimestampMillis(987654321L);
+        // Persist to file
+        mIndexerSettings.persist();
+
+        // Reset the settings to ensure loading happens from the file
+        mIndexerSettings.setLastUpdateTimestampMillis(0);
+        mIndexerSettings.setLastAppUpdateTimestampMillis(0);
+
+        // Load from file
+        mIndexerSettings.load();
+
+        // Check values after loading
+        Assert.assertEquals(123456789L, mIndexerSettings.getLastUpdateTimestampMillis());
+        Assert.assertEquals(987654321L, mIndexerSettings.getLastAppUpdateTimestampMillis());
+    }
+
+    @Test
+    public void testReset() {
+        mIndexerSettings.setLastUpdateTimestampMillis(123456789L);
+        mIndexerSettings.setLastAppUpdateTimestampMillis(987654321L);
+        mIndexerSettings.reset();
+        Assert.assertEquals(0, mIndexerSettings.getLastUpdateTimestampMillis());
+        Assert.assertEquals(0, mIndexerSettings.getLastAppUpdateTimestampMillis());
+    }
+}
+;
+
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerTestBase.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerTestBase.java
new file mode 100644
index 0000000..bad2645
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerTestBase.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.observer.DocumentChangeInfo;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.observer.SchemaChangeInfo;
+import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import org.junit.After;
+import org.junit.Before;
+
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AppsIndexerTestBase {
+    protected GlobalSearchSessionShim mShim;
+    protected ObserverCallback mCallback;
+    protected BroadcastReceiver mCapturedReceiver;
+    private static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    protected Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext =
+                new ContextWrapper(ApplicationProvider.getApplicationContext()) {
+
+                    @Nullable
+                    @Override
+                    public Intent registerReceiverForAllUsers(
+                            @Nullable BroadcastReceiver receiver,
+                            @NonNull IntentFilter filter,
+                            @Nullable String broadcastPermission,
+                            @Nullable Handler scheduler) {
+                        mCapturedReceiver = receiver;
+                        return super.registerReceiverForAllUsers(
+                                receiver,
+                                filter,
+                                broadcastPermission,
+                                scheduler,
+                                RECEIVER_EXPORTED);
+                    }
+                };
+        mShim = GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync(mContext).get();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mShim != null && mCallback != null) {
+            mShim.unregisterObserverCallback(mContext.getPackageName(), mCallback);
+        }
+    }
+
+    protected CountDownLatch setupLatch(int numChanges) throws Exception {
+        return setupLatch(numChanges, /* listenForSchemaChanges= */ false);
+    }
+
+    /**
+     * Sets up or resets the latch for observing changes, and registers a universal observer
+     * callback if it hasn't been registered before. The method configures the callback to listen
+     * for either schema or document changes based on the boolean parameter.
+     *
+     * @param numChanges the number of changes to count down
+     * @param listenForSchemaChanges if true, listens for schema changes; if false, listens for
+     *     document changes
+     */
+    protected CountDownLatch setupLatch(int numChanges, boolean listenForSchemaChanges)
+            throws Exception {
+        CountDownLatch latch = new CountDownLatch(numChanges);
+        // Unregister existing callback if any
+        if (mCallback != null) {
+            mShim.unregisterObserverCallback(mContext.getPackageName(), mCallback);
+        }
+        mCallback =
+                new ObserverCallback() {
+                    @Override
+                    public void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo) {
+                        if (!listenForSchemaChanges) {
+                            return;
+                        }
+                        // When we delete apps, we delete the schema.
+                        Set<String> changedSchemas = changeInfo.getChangedSchemaNames();
+                        for (String changedSchema : changedSchemas) {
+                            if (changedSchema.startsWith(MobileApplication.SCHEMA_TYPE)) {
+                                latch.countDown();
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo) {
+                        if (listenForSchemaChanges) {
+                            return;
+                        }
+                        if (!changeInfo.getSchemaName().startsWith(MobileApplication.SCHEMA_TYPE)) {
+                            return;
+                        }
+                        for (int i = 0; i < changeInfo.getChangedDocumentIds().size(); i++) {
+                            latch.countDown();
+                        }
+                    }
+                };
+        mShim.registerObserverCallback(
+                mContext.getPackageName(), new ObserverSpec.Builder().build(), EXECUTOR, mCallback);
+        return latch;
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerUserInstanceTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerUserInstanceTest.java
new file mode 100644
index 0000000..cf0dd6d
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsIndexerUserInstanceTest.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakePackageInfos;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeResolveInfos;
+import static com.android.server.appsearch.appsindexer.TestUtils.setupMockPackageManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.testutil.AppSearchSessionShimImpl;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class AppsIndexerUserInstanceTest extends AppsIndexerTestBase {
+    private TestContext mContext;
+    private final PackageManager mMockPackageManager = mock(PackageManager.class);
+
+    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+    private ThreadPoolExecutor mSingleThreadedExecutor;
+    private File mAppsDir;
+    private File mSettingsFile;
+    private AppsIndexerUserInstance mInstance;
+    private final AppsIndexerConfig mAppsIndexerConfig = new TestAppsIndexerConfig();
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        Context context = ApplicationProvider.getApplicationContext();
+        mContext = new TestContext(context);
+
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>());
+
+        // Setup the file path to the persisted data
+        mAppsDir = new File(mTemporaryFolder.newFolder(), "appsearch/apps");
+        mSettingsFile = new File(mAppsDir, AppsIndexerSettings.SETTINGS_FILE_NAME);
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+    }
+
+    @After
+    @Override
+    public void tearDown() throws Exception {
+        TestUtils.removeFakePackageDocuments(mContext, Executors.newSingleThreadExecutor());
+        mSingleThreadedExecutor.shutdownNow();
+        mInstance.shutdown();
+        super.tearDown();
+    }
+
+    @Test
+    public void testFirstRun_schedulesUpdate() throws Exception {
+        // This semaphore allows us to pause test execution until we're sure the tasks in
+        // AppsIndexerUserInstance are finished.
+        final Semaphore semaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        semaphore.release();
+                    }
+                };
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+
+        // Pretend there's one package on device
+        setupMockPackageManager(
+                mMockPackageManager, createFakePackageInfos(1), createFakeResolveInfos(1));
+
+        // Wait for file setup, as file setup uses the same ExecutorService.
+        semaphore.acquire();
+
+        long beforeFirstRun = mSingleThreadedExecutor.getCompletedTaskCount();
+
+        mInstance.updateAsync(true);
+        semaphore.acquire();
+
+        while (mSingleThreadedExecutor.getCompletedTaskCount() != beforeFirstRun + 1) {
+            continue;
+        }
+
+        assertThat(mSingleThreadedExecutor.getCompletedTaskCount()).isEqualTo(beforeFirstRun + 1);
+        try (AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext)) {
+            Map<String, Long> appsTimestampMap = searchHelper.getAppsFromAppSearch();
+            assertThat(appsTimestampMap).hasSize(1);
+            assertThat(appsTimestampMap.keySet()).containsExactly("com.fake.package0");
+        }
+    }
+
+    @Test
+    public void testFirstRun_updateAlreadyRan_doesNotUpdate() throws Exception {
+        // Pretend we already ran
+        AppsIndexerSettings settings = new AppsIndexerSettings(mAppsDir);
+        settings.setLastUpdateTimestampMillis(1000);
+        settings.persist();
+
+        // This semaphore allows us to pause test execution until we're sure the tasks in
+        // AppsIndexerUserInstance are finished.
+        final Semaphore semaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        semaphore.release();
+                    }
+                };
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+
+        // Pretend there's one package on device
+        setupMockPackageManager(
+                mMockPackageManager, createFakePackageInfos(1), createFakeResolveInfos(1));
+
+        // Wait for file setup, as file setup uses the same ExecutorService.
+        semaphore.acquire();
+
+        long beforeFirstRun = mSingleThreadedExecutor.getCompletedTaskCount();
+
+        mInstance.updateAsync(true);
+        // Wait for the task to finish
+        semaphore.acquire();
+
+        while (mSingleThreadedExecutor.getCompletedTaskCount() != beforeFirstRun + 1) {
+            continue;
+        }
+        // One more task should've ran, checked settings, and exited
+        assertThat(mSingleThreadedExecutor.getActiveCount()).isEqualTo(0);
+        assertThat(mSingleThreadedExecutor.getTaskCount()).isEqualTo(beforeFirstRun + 1);
+        assertThat(mSingleThreadedExecutor.getCompletedTaskCount()).isEqualTo(beforeFirstRun + 1);
+
+        // Even though a task ran and we got 1 app ready, we requested a "firstRun" but the
+        // timestamp was not 0, so nothing should've been indexed
+        try (AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext)) {
+            assertThat(searchHelper.getAppsFromAppSearch()).isEmpty();
+        }
+    }
+
+    @Test
+    public void testHandleMultipleNotifications_onlyOneUpdateCanBeScheduledAndRun()
+            throws Exception {
+        // This semaphore allows us to make sure that a sync has finished running before performing
+        // checks.
+        final Semaphore afterSemaphore = new Semaphore(0);
+        // This semaphore is released when the modified context calls getPackageManager, which is
+        // part of the sync. By waiting to acquire this in the test thread, we can ensure that we
+        // end up in the middle of the sync operation
+        final Semaphore midSyncSemaphoreA = new Semaphore(0);
+        // This semaphore blocks getPackageManager in the modified context, and continues when the
+        // test thread releases this semaphore. In the test thread, by waiting for
+        // midSyncSemaphoreA, running test code, then releasing midSyncSemaphoreB, we can guarantee
+        // that the test code runs in the middle of a sync, no timing required.
+        final Semaphore midSyncSemaphoreB = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        afterSemaphore.release();
+                    }
+                };
+
+        // We need to pause this mid-update so that we can schedule updates mid-update. We can do so
+        // by using a semaphore when we get package manager
+        Context pauseContext =
+                new TestContext(ApplicationProvider.getApplicationContext()) {
+                    @Override
+                    public PackageManager getPackageManager() {
+                        // Pause here with semaphore
+                        try {
+                            midSyncSemaphoreA.release();
+                            midSyncSemaphoreB.acquire();
+                        } catch (InterruptedException ignored) {
+                        }
+                        return mMockPackageManager;
+                    }
+                };
+
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        pauseContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+        // Wait for file setup, as file setup uses the same ExecutorService.
+        afterSemaphore.acquire();
+
+        int numOfNotifications = 20;
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(numOfNotifications / 10),
+                createFakeResolveInfos(numOfNotifications / 10));
+
+        // Schedule a bunch of tasks. However, only one will run, and one other will be scheduled
+        for (int i = 0; i < numOfNotifications / 2; i++) {
+            // This will pretend to add apps repeatedly
+            mInstance.updateAsync(/* firstRun= */ false);
+        }
+
+        // Now, we wait for getPackageManager to be called
+        midSyncSemaphoreA.acquire();
+
+        // We are now in the middle of the sync. The thread should be currently handling one sync.
+        // And the other (we allow two) should be scheduled.
+
+        // Settings task + current sync + scheduled second sync = 3
+        assertThat(mSingleThreadedExecutor.getTaskCount()).isEqualTo(3);
+        assertThat(mSingleThreadedExecutor.getActiveCount()).isEqualTo(1);
+        // Settings task
+        assertThat(mSingleThreadedExecutor.getCompletedTaskCount()).isEqualTo(1);
+
+        // Schedule even more sync
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(numOfNotifications),
+                createFakeResolveInfos(numOfNotifications));
+        for (int i = numOfNotifications / 2; i < numOfNotifications; i++) {
+            mInstance.updateAsync(/* firstRun= */ false);
+        }
+
+        // Now we allow syncing to continue
+        midSyncSemaphoreB.release();
+
+        // Wait for the first sync to finish
+        afterSemaphore.acquire();
+
+        // The call to getCompletedTaskCount can be flaky due to the fact that getCompletedTaskCount
+        // relies on a count that is updated a little bit AFTER afterExecute is called, which is
+        // where the semaphore is released. See ThreadPoolExecutor#runWorker
+        while (mSingleThreadedExecutor.getCompletedTaskCount() != 2) {
+            continue;
+        }
+
+        assertThat(mSingleThreadedExecutor.getCompletedTaskCount()).isEqualTo(2);
+
+        // Wait for the second sync to finish
+        midSyncSemaphoreB.release();
+        afterSemaphore.acquire();
+
+        // Only two updates ran even though many were scheduled
+        while (mSingleThreadedExecutor.getCompletedTaskCount() != 3) {
+            continue;
+        }
+        assertThat(mSingleThreadedExecutor.getCompletedTaskCount()).isEqualTo(3);
+        assertThat(mSingleThreadedExecutor.getActiveCount()).isEqualTo(0);
+
+        // Just to be sure
+        midSyncSemaphoreB.release(numOfNotifications);
+        afterSemaphore.release(numOfNotifications);
+
+        assertThat(mSingleThreadedExecutor.getActiveCount()).isEqualTo(0);
+        assertThat(mSingleThreadedExecutor.getCompletedTaskCount()).isEqualTo(3);
+    }
+
+    @Test
+    public void testCreateInstance_dataDirectoryCreatedAsynchronously() throws Exception {
+        File dataDir = new File(mTemporaryFolder.newFolder(), "apps");
+        boolean isDataDirectoryCreatedSynchronously =
+                mSingleThreadedExecutor
+                        .submit(
+                                () -> {
+                                    AppsIndexerUserInstance unused =
+                                            AppsIndexerUserInstance.createInstance(
+                                                    mContext,
+                                                    dataDir,
+                                                    mAppsIndexerConfig,
+                                                    mSingleThreadedExecutor);
+                                    // Data directory shouldn't have been created synchronously in
+                                    // createInstance()
+                                    return dataDir.exists();
+                                })
+                        .get();
+        assertThat(isDataDirectoryCreatedSynchronously).isFalse();
+        boolean isDataDirectoryCreatedAsynchronously =
+                mSingleThreadedExecutor.submit(dataDir::exists).get();
+        assertThat(isDataDirectoryCreatedAsynchronously).isTrue();
+    }
+
+    @Test
+    public void testUpdate() throws Exception {
+        int docCount = 500;
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(docCount),
+                createFakeResolveInfos(docCount));
+        CountDownLatch latch = setupLatch(docCount);
+
+        mInstance.doUpdate(/* firstRun= */ false);
+        latch.await(10, TimeUnit.SECONDS);
+
+        AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+        Map<String, Long> appIds = searchHelper.getAppsFromAppSearch();
+        assertThat(appIds.size()).isEqualTo(docCount);
+    }
+
+    @Test
+    public void testUpdate_setsLastAppUpdatedTimestamp() throws Exception {
+        int docCount = 10;
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(docCount),
+                createFakeResolveInfos(docCount));
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        AppsIndexerSettings settings = new AppsIndexerSettings(mAppsDir);
+        settings.load();
+        // The tenth document will have a timestamp of 9 as it is 0-indexed
+        assertThat(settings.getLastAppUpdateTimestampMillis()).isEqualTo(9);
+    }
+
+    @Test
+    public void testUpdate_insertedAndDeletedApps() throws Exception {
+        long timeBeforeChangeNotification = System.currentTimeMillis();
+        // Don't want to get this confused with real indexed packages.
+
+        // We can't actually install 10 apps here, then delete four them. So what we do is pretend
+        // to install 10 apps, run the indexer, then pretend there's only 6 apps, and run the
+        // indexer again. The indexer should create 10 MobileApplication documents, then remove four
+        // of them when we "remove" four apps.
+
+        setupMockPackageManager(
+                mMockPackageManager, createFakePackageInfos(10), createFakeResolveInfos(10));
+
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+        Map<String, Long> appIds = searchHelper.getAppsFromAppSearch();
+        assertThat(appIds.size()).isEqualTo(10);
+
+        setupMockPackageManager(
+                mMockPackageManager, createFakePackageInfos(6), createFakeResolveInfos(6));
+
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        searchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+        appIds = searchHelper.getAppsFromAppSearch();
+        assertThat(appIds.size()).isEqualTo(6);
+        assertThat(appIds.keySet())
+                .containsNoneOf(
+                        TestUtils.FAKE_PACKAGE_PREFIX + "6",
+                        TestUtils.FAKE_PACKAGE_PREFIX + "7",
+                        TestUtils.FAKE_PACKAGE_PREFIX + "8",
+                        TestUtils.FAKE_PACKAGE_PREFIX + "9");
+
+        PersistableBundle settingsBundle = AppsIndexerSettings.readBundle(mSettingsFile);
+        assertThat(settingsBundle.getLong(AppsIndexerSettings.LAST_UPDATE_TIMESTAMP_KEY))
+                .isAtLeast(timeBeforeChangeNotification);
+
+        // The last updated app was still the "9" app
+        assertThat(settingsBundle.getLong(AppsIndexerSettings.LAST_APP_UPDATE_TIMESTAMP_KEY))
+                .isEqualTo(9);
+    }
+
+    @Test
+    public void testStart_initialRun_schedulesUpdateJob() throws Exception {
+        JobScheduler mockJobScheduler = mock(JobScheduler.class);
+        mContext.setJobScheduler(mockJobScheduler);
+        // This semaphore allows us to make sure that a sync has finished running before performing
+        // checks.
+        final Semaphore afterSemaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        afterSemaphore.release();
+                    }
+                };
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+        // Wait for settings initialization
+        afterSemaphore.acquire();
+
+        int docCount = 100;
+        // Set up package manager
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(docCount),
+                createFakeResolveInfos(docCount));
+
+        mInstance.updateAsync(/* firstRun= */ true);
+
+        // Wait for all async tasks to complete
+        afterSemaphore.acquire();
+
+        ArgumentCaptor<JobInfo> jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mockJobScheduler).schedule(jobInfoArgumentCaptor.capture());
+        JobInfo updateJob = jobInfoArgumentCaptor.getValue();
+        assertThat(updateJob.isRequireBatteryNotLow()).isTrue();
+        assertThat(updateJob.isRequireDeviceIdle()).isTrue();
+        assertThat(updateJob.isPersisted()).isTrue();
+        assertThat(updateJob.isPeriodic()).isTrue();
+    }
+
+    @Test
+    public void testStart_subsequentRunWithNoScheduledJob_schedulesUpdateJob() throws Exception {
+        // Trigger an initial update.
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        // This semaphore allows us to pause test execution until we're sure the tasks in
+        // AppsIndexerUserInstance (scheduling the maintenance job) are finished.
+        final Semaphore semaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        semaphore.release();
+                    }
+                };
+
+        // By default mockJobScheduler.getPendingJob() would return null. This simulates the
+        // scenario where the scheduled update job after the initial run is cancelled
+        // due to some reason.
+        JobScheduler mockJobScheduler = mock(JobScheduler.class);
+        mContext.setJobScheduler(mockJobScheduler);
+        // the update should be zero, and if not it's because of mAppsDir
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+
+        // Wait for file setup, as file setup uses the same ExecutorService.
+        semaphore.acquire();
+
+        int docCount = 100;
+        // Set up package manager
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(docCount),
+                createFakeResolveInfos(docCount));
+
+        mInstance.updateAsync(/* firstRun= */ false);
+
+        // Wait for all async tasks to complete
+        semaphore.acquire();
+
+        ArgumentCaptor<JobInfo> jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mockJobScheduler).schedule(jobInfoArgumentCaptor.capture());
+        JobInfo updateJob = jobInfoArgumentCaptor.getValue();
+        assertThat(updateJob.isRequireBatteryNotLow()).isTrue();
+        assertThat(updateJob.isRequireDeviceIdle()).isTrue();
+        assertThat(updateJob.isPersisted()).isTrue();
+        assertThat(updateJob.isPeriodic()).isTrue();
+    }
+
+    @Test
+    public void testUpdate_triggered_afterCompatibleSchemaChange() throws Exception {
+        // Preset a compatible schema.
+        AppSearchManager.SearchContext searchContext =
+                new AppSearchManager.SearchContext.Builder(AppSearchHelper.APP_DATABASE).build();
+        AppSearchSessionShim db =
+                AppSearchSessionShimImpl.createSearchSessionAsync(searchContext).get();
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(TestUtils.COMPATIBLE_APP_SCHEMA)
+                        .setForceOverride(true)
+                        .build();
+        db.setSchemaAsync(setSchemaRequest).get();
+        db.close();
+
+        // The current schema is compatible, and an update will be triggered
+        JobScheduler mockJobScheduler = mock(JobScheduler.class);
+        mContext.setJobScheduler(mockJobScheduler);
+        // This semaphore allows us to make sure that a sync has finished running before performing
+        // checks.
+        final Semaphore afterSemaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        afterSemaphore.release();
+                    }
+                };
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+        // Wait for settings initialization
+        afterSemaphore.acquire();
+
+        mInstance.updateAsync(/* firstRun= */ true);
+        afterSemaphore.acquire();
+
+        ArgumentCaptor<JobInfo> jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mockJobScheduler).schedule(jobInfoArgumentCaptor.capture());
+        JobInfo updateJob = jobInfoArgumentCaptor.getValue();
+        assertThat(updateJob.isRequireBatteryNotLow()).isTrue();
+        assertThat(updateJob.isRequireDeviceIdle()).isTrue();
+        assertThat(updateJob.isPersisted()).isTrue();
+        assertThat(updateJob.isPeriodic()).isTrue();
+    }
+
+    @Test
+    public void testUpdate_triggered_afterIncompatibleSchemaChange() throws Exception {
+        int docCount = 250;
+
+        // This semaphore allows us to pause test execution until we're sure the tasks in
+        // AppsIndexerUserInstance (scheduling the maintenance job) are finished.
+        final Semaphore semaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        semaphore.release();
+                    }
+                };
+
+        // Preset an incompatible schema.
+        AppSearchManager.SearchContext searchContext =
+                new AppSearchManager.SearchContext.Builder(AppSearchHelper.APP_DATABASE).build();
+        AppSearchSessionShim db =
+                AppSearchSessionShimImpl.createSearchSessionAsync(searchContext).get();
+
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(TestUtils.INCOMPATIBLE_APP_SCHEMA)
+                        .setForceOverride(true)
+                        .build();
+        db.setSchemaAsync(setSchemaRequest).get();
+
+        // Since the current schema is incompatible, it will overwrite it
+        JobScheduler mockJobScheduler = mock(JobScheduler.class);
+        mContext.setJobScheduler(mockJobScheduler);
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+        // Wait for file setup, as file setup uses the same ExecutorService.
+        semaphore.acquire();
+
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(docCount),
+                createFakeResolveInfos(docCount));
+
+        mInstance.updateAsync(/* firstRun= */ true);
+        // Wait for all async tasks to complete
+        semaphore.acquire();
+
+        ArgumentCaptor<JobInfo> jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
+        verify(mockJobScheduler).schedule(jobInfoArgumentCaptor.capture());
+        JobInfo updateJob = jobInfoArgumentCaptor.getValue();
+        assertThat(updateJob.isRequireBatteryNotLow()).isTrue();
+        assertThat(updateJob.isRequireDeviceIdle()).isTrue();
+        assertThat(updateJob.isPersisted()).isTrue();
+        assertThat(updateJob.isPeriodic()).isTrue();
+    }
+
+    @Test
+    public void testConcurrentUpdates_updatesDoNotInterfereWithEachOther() throws Exception {
+        long timeBeforeChangeNotification = System.currentTimeMillis();
+        setupMockPackageManager(
+                mMockPackageManager, createFakePackageInfos(250), createFakeResolveInfos(250));
+        // This semaphore allows us to make sure that a sync has finished running before performing
+        // checks.
+        final Semaphore afterSemaphore = new Semaphore(0);
+        mSingleThreadedExecutor =
+                new ThreadPoolExecutor(
+                        /* corePoolSize= */ 1,
+                        /* maximumPoolSize= */ 1,
+                        /* KeepAliveTime= */ 0L,
+                        TimeUnit.MILLISECONDS,
+                        new LinkedBlockingQueue<>()) {
+                    @Override
+                    protected void afterExecute(Runnable r, Throwable t) {
+                        super.afterExecute(r, t);
+                        afterSemaphore.release();
+                    }
+                };
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+        // Wait for settings initialization
+        afterSemaphore.acquire();
+
+        // As there is nothing else in the executor queue, it should run soon.
+        Future<?> unused =
+                mSingleThreadedExecutor.submit(() -> mInstance.doUpdate(/* firstRun= */ false));
+
+        // On the current thread, this update will run at the same time as the task on the executor.
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        // By waiting for the single threaded executor to finish after calling doUpdate, both
+        // updates are guaranteed to be finished.
+        afterSemaphore.acquire();
+
+        AppSearchHelper searchHelper = AppSearchHelper.createAppSearchHelper(mContext);
+        Map<String, Long> appIds = searchHelper.getAppsFromAppSearch();
+        assertThat(appIds.size()).isEqualTo(250);
+
+        PersistableBundle settingsBundle = AppsIndexerSettings.readBundle(mSettingsFile);
+        assertThat(settingsBundle.getLong(AppsIndexerSettings.LAST_UPDATE_TIMESTAMP_KEY))
+                .isAtLeast(timeBeforeChangeNotification);
+    }
+
+    @Test
+    public void testStart_subsequentRunWithScheduledJob_doesNotScheduleUpdateJob()
+            throws Exception {
+        // Trigger an initial update.
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        JobScheduler mockJobScheduler = mock(JobScheduler.class);
+        JobInfo mockJobInfo = mock(JobInfo.class);
+        // getPendingJob() should return a non-null value to simulate the scenario where a
+        // background job is already scheduled.
+        doReturn(mockJobInfo)
+                .when(mockJobScheduler)
+                .getPendingJob(
+                        AppsIndexerMaintenanceConfig.MIN_APPS_INDEXER_JOB_ID
+                                + mContext.getUser().getIdentifier());
+        mContext.setJobScheduler(mockJobScheduler);
+        mInstance =
+                AppsIndexerUserInstance.createInstance(
+                        mContext, mAppsDir, mAppsIndexerConfig, mSingleThreadedExecutor);
+
+        int docCount = 10;
+        CountDownLatch latch = setupLatch(docCount);
+        setupMockPackageManager(
+                mMockPackageManager,
+                createFakePackageInfos(docCount),
+                createFakeResolveInfos(docCount));
+        mInstance.doUpdate(/* firstRun= */ false);
+
+        mInstance.updateAsync(/* firstRun= */ false);
+
+        // Wait for all async tasks to complete
+        latch.await(10L, TimeUnit.SECONDS);
+
+        verify(mockJobScheduler, never()).schedule(any());
+    }
+
+    class TestContext extends ContextWrapper {
+        @Nullable JobScheduler mJobScheduler;
+
+        TestContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        @Nullable
+        public Object getSystemService(String name) {
+            if (mJobScheduler != null && Context.JOB_SCHEDULER_SERVICE.equals(name)) {
+                return mJobScheduler;
+            }
+            return getBaseContext().getSystemService(name);
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mMockPackageManager;
+        }
+
+        public void setJobScheduler(@Nullable JobScheduler jobScheduler) {
+            mJobScheduler = jobScheduler;
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return this;
+        }
+
+        @Override
+        @NonNull
+        public Context createContextAsUser(UserHandle user, int flags) {
+            return this;
+        }
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java
new file mode 100644
index 0000000..166a815
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakePackageInfo;
+import static com.android.server.appsearch.appsindexer.TestUtils.createFakeResolveInfo;
+import static com.android.server.appsearch.appsindexer.TestUtils.setupMockPackageManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.util.ArrayMap;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** This tests that we can convert what comes from PackageManager to a MobileApplication */
+public class AppsUtilTest {
+    @Test
+    public void testBuildAppsFromPackageInfos_ReturnsNonNullList() throws Exception {
+        PackageManager pm = Mockito.mock(PackageManager.class);
+        // Populate fake PackageManager with 10 Packages.
+        List<PackageInfo> fakePackages = new ArrayList<>();
+        List<ResolveInfo> fakeActivities = new ArrayList<>();
+        Map<PackageInfo, ResolveInfo> packageActivityMapping = new ArrayMap<>();
+
+        for (int i = 0; i < 10; i++) {
+            fakePackages.add(createFakePackageInfo(i));
+            fakeActivities.add(createFakeResolveInfo(i));
+        }
+
+        // Package manager "has" 10 fake packages, but we're choosing just 5 of them to simulate the
+        // case that not all the apps need to be synced. For example, 5 new apps were added and the
+        // rest of the existing apps don't need to be re-indexed.
+        for (int i = 0; i < 5; i++) {
+            packageActivityMapping.put(fakePackages.get(i), fakeActivities.get(i));
+        }
+
+        setupMockPackageManager(pm, fakePackages, fakeActivities);
+        List<MobileApplication> resultApps =
+                AppsUtil.buildAppsFromPackageInfos(pm, packageActivityMapping);
+
+        assertThat(resultApps).hasSize(5);
+        List<String> packageNames = new ArrayList<>();
+        for (int i = 0; i < resultApps.size(); i++) {
+            packageNames.add(resultApps.get(i).getPackageName());
+        }
+        assertThat(packageNames)
+                .containsExactly(
+                        "com.fake.package0",
+                        "com.fake.package1",
+                        "com.fake.package2",
+                        "com.fake.package3",
+                        "com.fake.package4");
+    }
+
+    @Test
+    public void testBuildRealApps() {
+        // This shouldn't crash, and shouldn't be an empty list
+        Context context = ApplicationProvider.getApplicationContext();
+        Map<PackageInfo, ResolveInfo> packageActivityMapping =
+                AppsUtil.getLaunchablePackages(context.getPackageManager());
+        List<MobileApplication> resultApps =
+                AppsUtil.buildAppsFromPackageInfos(
+                        context.getPackageManager(), packageActivityMapping);
+
+        assertThat(resultApps).isNotEmpty();
+        assertThat(resultApps.get(0).getDisplayName()).isNotEmpty();
+    }
+}
+
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/SyncAppSearchImplTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/SyncAppSearchImplTest.java
new file mode 100644
index 0000000..eaed6e4
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/SyncAppSearchImplTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static android.app.appsearch.SearchSpec.TERM_MATCH_PREFIX;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.app.appsearch.AppSearchBatchResult;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+/** Tests for {@link SyncAppSearchSessionImpl}. */
+public class SyncAppSearchImplTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final AppSearchManager mAppSearch = mContext.getSystemService(AppSearchManager.class);
+    private final Executor mExecutor = mContext.getMainExecutor();
+
+    @Before
+    public void setUp() throws Exception {
+        Objects.requireNonNull(mAppSearch);
+        clean();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+       clean();
+    }
+
+    private void clean() throws Exception {
+        // Remove all documents from any instances that may have been created in the tests.
+        AppSearchManager.SearchContext searchContext =
+                new AppSearchManager.SearchContext.Builder("testDb").build();
+        CompletableFuture<AppSearchResult<AppSearchSession>> future = new CompletableFuture<>();
+        mAppSearch.createSearchSession(searchContext, mExecutor, future::complete);
+        AppSearchSession searchSession = future.get().getResultValue();
+        CompletableFuture<AppSearchResult<SetSchemaResponse>> schemaFuture =
+                new CompletableFuture<>();
+        searchSession.setSchema(
+                new SetSchemaRequest.Builder().setForceOverride(true).build(), mExecutor, mExecutor,
+                schemaFuture::complete);
+        schemaFuture.get().getResultValue();
+    }
+
+    @Test
+    public void testSynchronousMethods() throws Exception {
+        AppSearchManager.SearchContext searchContext =
+                new AppSearchManager.SearchContext.Builder("testDb").build();
+
+        SyncAppSearchSession syncWrapper =
+                new SyncAppSearchSessionImpl(mAppSearch, searchContext, mExecutor);
+
+        // Set the schema.
+        syncWrapper.setSchema(new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("schema1").build())
+                .setForceOverride(true).build());
+
+        // Create a document and insert 3 package1 documents
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schema1").build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id2",
+                "schema1").build();
+        GenericDocument document3 = new GenericDocument.Builder<>("namespace", "id3",
+                "schema1").build();
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addGenericDocuments(document1, document2, document3).build();
+        // Test put operation with no futures
+        AppSearchBatchResult<String, Void> result = syncWrapper.put(request);
+
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getSuccesses()).hasSize(3);
+
+        SyncGlobalSearchSession globalSession =
+                new SyncGlobalSearchSessionImpl(mAppSearch, mExecutor);
+        // Search globally for only 2 result per page
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(TERM_MATCH_PREFIX)
+                .addFilterPackageNames(mContext.getPackageName())
+                .setResultCountPerPage(2)
+                .build();
+        SyncSearchResults searchResults = globalSession.search("", searchSpec);
+
+        // Get the first page, it contains 2 results.
+        List<GenericDocument> outDocs = new ArrayList<>();
+        List<SearchResult> results = searchResults.getNextPage();
+        assertThat(results).hasSize(2);
+        outDocs.add(results.get(0).getGenericDocument());
+        outDocs.add(results.get(1).getGenericDocument());
+
+        // Get the second page, it contains only 1 result.
+        results = searchResults.getNextPage();
+        assertThat(results).hasSize(1);
+        outDocs.add(results.get(0).getGenericDocument());
+
+        assertThat(outDocs).containsExactly(document1, document2, document3);
+
+        // We get all documents, and it shouldn't fail if we keep calling getNextPage().
+        results = searchResults.getNextPage();
+        assertThat(results).isEmpty();
+
+        // Check that we can keep using the global session
+        searchSpec =
+                new SearchSpec.Builder()
+                        .setTermMatch(TERM_MATCH_PREFIX)
+                        .addFilterPackageNames(mContext.getPackageName())
+                        .setResultCountPerPage(3)
+                        .build();
+        searchResults = globalSession.search("", searchSpec);
+        results = searchResults.getNextPage();
+
+        outDocs.clear();
+        outDocs.add(results.get(0).getGenericDocument());
+        outDocs.add(results.get(1).getGenericDocument());
+        outDocs.add(results.get(2).getGenericDocument());
+        assertThat(outDocs).containsExactly(document1, document2, document3);
+    }
+
+    @Test
+    public void testClosedCallbackExecutor() {
+        ExecutorService callbackExecutor = Executors.newSingleThreadExecutor();
+        callbackExecutor.shutdown();
+        AppSearchManager.SearchContext searchContext =
+                new AppSearchManager.SearchContext.Builder("testDb").build();
+        assertThrows(RejectedExecutionException.class, () ->
+                new SyncAppSearchSessionImpl(mAppSearch, searchContext, callbackExecutor));
+    }
+}
\ No newline at end of file
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestAppsIndexerConfig.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestAppsIndexerConfig.java
new file mode 100644
index 0000000..89c7da2
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestAppsIndexerConfig.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+public class TestAppsIndexerConfig implements AppsIndexerConfig {
+    @Override
+    public boolean isAppsIndexerEnabled() {
+        return true;
+    }
+
+    @Override
+    public long getAppsMaintenanceUpdateIntervalMillis() {
+        return 24 * 60 * 60 * 1000L;
+    }
+}
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java
new file mode 100644
index 0000000..9f01458
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer;
+
+import static com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication.SCHEMA_TYPE;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSessionShim;
+import android.app.appsearch.GlobalSearchSessionShim;
+import android.app.appsearch.PackageIdentifier;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResultsShim;
+import android.app.appsearch.SearchSpec;
+import android.app.appsearch.SetSchemaRequest;
+import android.app.appsearch.SetSchemaResponse;
+import android.app.appsearch.testutil.AppSearchSessionShimImpl;
+import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.content.res.Resources;
+
+import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+class TestUtils {
+    // In the mocking tests, integers are appended to this prefix to create unique package names.
+    public static final String FAKE_PACKAGE_PREFIX = "com.fake.package";
+    public static final Signature FAKE_SIGNATURE = new Signature("deadbeef");
+
+    // Represents a schema compatible with MobileApplication. This is used to test compatible schema
+    // upgrades. It is compatible as changing to MobileApplication just adds properties.
+    public static final AppSearchSchema COMPATIBLE_APP_SCHEMA =
+            new AppSearchSchema.Builder(SCHEMA_TYPE)
+                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                            MobileApplication.APP_PROPERTY_PACKAGE_NAME)
+                            .setCardinality(
+                                    AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                            .setIndexingType(
+                                    AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                            .setTokenizerType(
+                                    AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
+                            .build())
+                    .build();
+
+    // Represents a schema incompatible with MobileApplication. This is used to test incompatible
+    // schema upgrades. It is incompatible as changing to MobileApplication removes the
+    // "NotPackageName" field.
+    public static final AppSearchSchema INCOMPATIBLE_APP_SCHEMA =
+            new AppSearchSchema.Builder(SCHEMA_TYPE)
+                    .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("NotPackageName")
+                            .setCardinality(
+                                    AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                            .setIndexingType(
+                                    AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                            .setTokenizerType(
+                                    AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                            .build())
+                    .build();
+
+    /**
+     * Creates a fake {@link PackageInfo} object.
+     *
+     * @param variant provides variation in the mocked PackageInfo so we can index multiple fake
+     *                apps.
+     */
+    @NonNull
+    public static PackageInfo createFakePackageInfo(int variant) {
+        String pkgName = FAKE_PACKAGE_PREFIX + variant;
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = pkgName;
+        packageInfo.versionName = "10.0.0";
+        packageInfo.lastUpdateTime = variant;
+        SigningInfo signingInfo = Mockito.mock(SigningInfo.class);
+        when(signingInfo.getSigningCertificateHistory())
+                .thenReturn(new Signature[] {FAKE_SIGNATURE});
+        packageInfo.signingInfo = signingInfo;
+
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.packageName = pkgName;
+        appInfo.className = pkgName + ".FakeActivity";
+        appInfo.name = "package" + variant;
+        appInfo.versionCode = 10;
+        packageInfo.applicationInfo = appInfo;
+
+        return packageInfo;
+    }
+
+    /**
+     * Creates multiple fake {@link PackageInfo} objects
+     *
+     * @param numApps number of PackageInfos to create.
+     * @see #createFakePackageInfo
+     */
+    @NonNull
+    public static List<PackageInfo> createFakePackageInfos(int numApps) {
+        List<PackageInfo> packageInfoList = new ArrayList<>();
+        for (int i = 0; i < numApps; i++) {
+            packageInfoList.add(createFakePackageInfo(i));
+        }
+        return packageInfoList;
+    }
+
+    /**
+     * Generates a mock resolve info corresponding to the same package created by
+     * {@link #createFakePackageInfo} with the same variant.
+     *
+     * @param variant adds variation in the mocked ResolveInfo so we can index multiple fake apps.
+     */
+    @NonNull
+    public static ResolveInfo createFakeResolveInfo(int variant) {
+        String pkgName = FAKE_PACKAGE_PREFIX + variant;
+        ResolveInfo mockResolveInfo = new ResolveInfo();
+        mockResolveInfo.activityInfo = new ActivityInfo();
+        mockResolveInfo.activityInfo.packageName = pkgName;
+        mockResolveInfo.activityInfo.name = pkgName + ".FakeActivity";
+        mockResolveInfo.activityInfo.icon = 42;
+
+        mockResolveInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        mockResolveInfo.activityInfo.applicationInfo.packageName = pkgName;
+        mockResolveInfo.activityInfo.applicationInfo.name = "Fake Application Name"; // Optional
+        return mockResolveInfo;
+    }
+
+    /**
+     * Generates multiple mock ResolveInfos.
+     *
+     * @see #createFakeResolveInfo
+     * @param numApps number of mock ResolveInfos to create
+     */
+    @NonNull
+    public static List<ResolveInfo> createFakeResolveInfos(int numApps) {
+        List<ResolveInfo> resolveInfoList = new ArrayList<>();
+        for (int i = 0; i < numApps; i++) {
+            resolveInfoList.add(createFakeResolveInfo(i));
+        }
+        return resolveInfoList;
+    }
+
+    /**
+     * Configure a mock {@link PackageManager} to return certain {@link PackageInfo}s and
+     * {@link ResolveInfo}s when getInstalledPackages and queryIntentActivities are called,
+     * respectively.
+     */
+    public static void setupMockPackageManager(@NonNull PackageManager pm,
+            @NonNull List<PackageInfo> packages, @NonNull List<ResolveInfo> activities)
+            throws Exception {
+        Objects.requireNonNull(pm);
+        Objects.requireNonNull(packages);
+        Objects.requireNonNull(activities);
+        when(pm.getInstalledPackages(anyInt())).thenReturn(packages);
+        Resources res = Mockito.mock(Resources.class);
+        when(res.getResourcePackageName(anyInt())).thenReturn("idk");
+        when(res.getResourceTypeName(anyInt())).thenReturn("type");
+        when(pm.getResourcesForApplication((ApplicationInfo) any())).thenReturn(res);
+        when(pm.getApplicationLabel(any())).thenReturn("label");
+        when(pm.queryIntentActivities(any(), eq(0))).then(i -> activities);
+    }
+
+    /** Wipes out the apps database. */
+    public static void removeFakePackageDocuments(
+            @NonNull Context context, @NonNull ExecutorService executorService)
+            throws ExecutionException, InterruptedException {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(executorService);
+
+        AppSearchSessionShim db =
+                AppSearchSessionShimImpl.createSearchSessionAsync(
+                                context,
+                                new AppSearchManager.SearchContext.Builder("apps-db").build(),
+                                executorService)
+                        .get();
+
+        SetSchemaResponse unused =
+                db.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build())
+                        .get();
+    }
+
+    /**
+     * Search for documents indexed by the Apps Indexer. The database, namespace, and schematype are
+     * all configured.
+     * @param pageSize The page size to use in the {@link SearchSpec}. By setting to a expected
+     *                 amount + 1, you can verify that the expected quantity of apps docs are
+     *                 present.
+     */
+    @NonNull
+    public static List<SearchResult> searchAppSearchForApps(int pageSize)
+            throws ExecutionException, InterruptedException {
+        GlobalSearchSessionShim globalSession =
+                GlobalSearchSessionShimImpl.createGlobalSearchSessionAsync().get();
+        SearchSpec allDocumentIdsSpec =
+                new SearchSpec.Builder()
+                        .addFilterNamespaces(MobileApplication.APPS_NAMESPACE)
+                        // We don't want to search over real indexed apps here, just the ones in the
+                        // test
+                        .addFilterPackageNames("com.android.appsearch.appsindexertests")
+                        .addProjection(
+                                SearchSpec.SCHEMA_TYPE_WILDCARD,
+                                Collections.singletonList(
+                                        MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP))
+                        .setResultCountPerPage(pageSize)
+                        .build();
+        // Don't want to get this confused with real indexed apps.
+        SearchResultsShim results =
+                globalSession.search(/*queryExpression=*/ "com.fake.package", allDocumentIdsSpec);
+        return results.getNextPageAsync().get();
+    }
+
+    /**
+     * Creates an {@link AppSearchSessionShim} for the same database the apps indexer interacts with
+     * for mock packages. This is useful for verifying indexed documents and directly adding
+     * documents.
+     */
+    @NonNull
+    public static AppSearchSessionShim createFakeAppIndexerSession(
+            @NonNull Context context, @NonNull ExecutorService executorService)
+            throws ExecutionException, InterruptedException {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(executorService);
+        return AppSearchSessionShimImpl.createSearchSessionAsync(
+                        context,
+                        new AppSearchManager.SearchContext.Builder("apps-db").build(),
+                        executorService)
+                .get();
+    }
+
+    /**
+     * Generates a mock {@link MobileApplication} corresponding to the same package created by
+     * {@link #createFakePackageInfo} with the same variant.
+     *
+     * @param variant adds variation to the MobileApplication document.
+     */
+    @NonNull
+    public static MobileApplication createFakeMobileApplication(int variant) {
+        return new MobileApplication.Builder(
+                        FAKE_PACKAGE_PREFIX + variant, FAKE_SIGNATURE.toByteArray())
+                .setDisplayName("Fake Application Name")
+                .setIconUri("https://cs.android.com")
+                .setClassName(".class")
+                .setUpdatedTimestampMs(variant)
+                .setAlternateNames("Mock")
+                .build();
+    }
+
+    /**
+     * Generates multiple mock {@link MobileApplication} objects.
+     *
+     * @see #createFakeMobileApplication
+     */
+    @NonNull
+    public static List<MobileApplication> createMobileApplications(int numApps) {
+        List<MobileApplication> appList = new ArrayList<>();
+        for (int i = 0; i < numApps; i++) {
+            appList.add(createFakeMobileApplication(i));
+        }
+        return appList;
+    }
+
+    /**
+     * Returns a package identifier representing some mock package.
+     *
+     * @param variant Provides variety in the package name in the same manner as {@link
+     *     #createFakePackageInfo} and {@link #createFakeMobileApplication}
+     */
+    @NonNull
+    public static PackageIdentifier createMockPackageIdentifier(int variant) {
+        return new PackageIdentifier(FAKE_PACKAGE_PREFIX + variant, FAKE_SIGNATURE.toByteArray());
+    }
+
+    /** Returns multiple package identifiers for use in testing. */
+    @NonNull
+    public static List<PackageIdentifier> createMockPackageIdentifiers(int numApps) {
+        List<PackageIdentifier> packageIdList = new ArrayList<>();
+        for (int i = 0; i < numApps; i++) {
+            packageIdList.add(createMockPackageIdentifier(i));
+        }
+        return packageIdList;
+    }
+}
+
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/appsearchtypes/MobileApplicationTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/appsearchtypes/MobileApplicationTest.java
new file mode 100644
index 0000000..478fb86
--- /dev/null
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/appsearchtypes/MobileApplicationTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch.appsindexer.appsearchtypes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+
+import org.junit.Test;
+
+public class MobileApplicationTest {
+    @Test
+    public void testMobileApplication() {
+        String packageName = "com.android.apps.food";
+        String className = "com.android.foodapp.SearchActivity";
+        String displayName = "The Food App";
+        String iconUri = "https://www.android.com/images/branding/product/1x/appg_24dp.png";
+        String[] alternateNames = {"Food", "Eat"};
+        long updatedTimestamp = System.currentTimeMillis();
+        byte[] sha256Certificate = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+        MobileApplication mobileApplication =
+                new MobileApplication.Builder(packageName, sha256Certificate)
+                        .setClassName(className)
+                        .setDisplayName(displayName)
+                        .setIconUri(iconUri)
+                        .setAlternateNames(alternateNames)
+                        .setUpdatedTimestampMs(updatedTimestamp)
+                        .build();
+
+        assertThat(mobileApplication.getPackageName()).isEqualTo(packageName);
+        assertThat(mobileApplication.getClassName()).isEqualTo(className);
+        assertThat(mobileApplication.getDisplayName()).isEqualTo(displayName);
+        assertThat(mobileApplication.getIconUri()).isEqualTo(Uri.parse(iconUri));
+        assertThat(mobileApplication.getAlternateNames()).isEqualTo(alternateNames);
+        assertThat(mobileApplication.getSha256Certificate()).isEqualTo(sha256Certificate);
+        assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(updatedTimestamp);
+    }
+}
diff --git a/testing/contactsindexertests/AndroidManifest.xml b/testing/contactsindexertests/AndroidManifest.xml
index 3f6bcfb..97e230a 100644
--- a/testing/contactsindexertests/AndroidManifest.xml
+++ b/testing/contactsindexertests/AndroidManifest.xml
@@ -23,6 +23,9 @@
     <application android:label="ContactsIndexerTests"
                  android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
+        <service android:name="com.android.server.appsearch.appsindexer.IndexerMaintenanceService"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
         <service android:name="com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService"
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
diff --git a/testing/contactsindexertests/AndroidTest.xml b/testing/contactsindexertests/AndroidTest.xml
index 3d7d38f..65c1b05 100644
--- a/testing/contactsindexertests/AndroidTest.xml
+++ b/testing/contactsindexertests/AndroidTest.xml
@@ -33,6 +33,7 @@
             value="com.android.bedstead.harrier.annotations.RequireRunOnWorkProfile" />
         <option name="exclude-annotation"
             value="com.android.bedstead.harrier.annotations.RequireRunOnSecondaryUser" />
+        <option name="hidden-api-checks" value="false" />
     </test>
 
     <object type="module_controller"
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceTest.java
index 0e8c780..4b57b51 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerMaintenanceTest.java
@@ -18,13 +18,13 @@
 
 import static android.Manifest.permission.RECEIVE_BOOT_COMPLETED;
 
-import static com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService.MIN_INDEXER_JOB_ID;
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.CONTACTS_INDEXER;
+import static com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceConfig.MIN_CONTACTS_INDEXER_JOB_ID;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
@@ -37,16 +37,24 @@
 import android.annotation.UserIdInt;
 import android.app.UiAutomation;
 import android.app.job.JobInfo;
+import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.pm.UserInfo;
 import android.os.CancellationSignal;
+import android.os.PersistableBundle;
+import android.os.UserHandle;
 
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.SystemService;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -54,10 +62,6 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
-import com.android.server.LocalManagerRegistry;
-import com.android.server.SystemService;
-
 import org.mockito.MockitoSession;
 
 import java.io.IOException;
@@ -66,12 +70,15 @@
 
 public class ContactsIndexerMaintenanceTest {
     private static final int DEFAULT_USER_ID = 0;
+    private static final UserHandle DEFAULT_USER_HANDLE = new UserHandle(0);
 
     private Context mContext = ApplicationProvider.getApplicationContext();
     private Context mContextWrapper;
-    private ContactsIndexerMaintenanceService mContactsIndexerMaintenanceService;
+    private IndexerMaintenanceService mIndexerMaintenanceService;
     private MockitoSession session;
     @Mock private JobScheduler mockJobScheduler;
+    private JobParameters mParams;
+    private PersistableBundle mExtras;
 
     @Before
     public void setUp() {
@@ -86,11 +93,14 @@
                 return getSystemService(name);
             }
         };
-        mContactsIndexerMaintenanceService = spy(new ContactsIndexerMaintenanceService());
-        doNothing().when(mContactsIndexerMaintenanceService).jobFinished(any(), anyBoolean());
+        mIndexerMaintenanceService = spy(new IndexerMaintenanceService());
+        doNothing().when(mIndexerMaintenanceService).jobFinished(any(), anyBoolean());
         session = ExtendedMockito.mockitoSession().
                 mockStatic(LocalManagerRegistry.class).
                 startMocking();
+        mExtras = new PersistableBundle();
+        mExtras.putInt("indexer_type", CONTACTS_INDEXER);
+        mParams = Mockito.mock(JobParameters.class);
     }
 
     @After
@@ -103,8 +113,12 @@
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
             uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
-            ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext, DEFAULT_USER_ID,
-                    /*periodic=*/ false, /*intervalMillis=*/ -1);
+            IndexerMaintenanceService.scheduleUpdateJob(
+                    mContext,
+                    DEFAULT_USER_HANDLE,
+                    CONTACTS_INDEXER,
+                    /* periodic= */ false,
+                    /* intervalMillis= */ -1);
         } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
@@ -121,8 +135,12 @@
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
             uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
-            ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext, /*userId=*/ 0,
-                    /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(7));
+            IndexerMaintenanceService.scheduleUpdateJob(
+                    mContext,
+                    /* userId= */ DEFAULT_USER_HANDLE,
+                    /* indexerType= */ CONTACTS_INDEXER,
+                    /* periodic= */ true,
+                    /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
         } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
@@ -138,15 +156,24 @@
 
     @Test
     public void testScheduleFullUpdateJob_oneOffThenPeriodic_isRescheduled() {
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContextWrapper, DEFAULT_USER_ID,
-                /*periodic=*/ false, /*intervalMillis=*/ -1);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ CONTACTS_INDEXER,
+                /* periodic= */ false,
+                /* intervalMillis= */ -1);
         ArgumentCaptor<JobInfo> firstJobInfoCaptor = ArgumentCaptor.forClass(JobInfo.class);
         verify(mockJobScheduler).schedule(firstJobInfoCaptor.capture());
         JobInfo firstJobInfo = firstJobInfoCaptor.getValue();
 
-        when(mockJobScheduler.getPendingJob(eq(MIN_INDEXER_JOB_ID))).thenReturn(firstJobInfo);
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContextWrapper, DEFAULT_USER_ID,
-                /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(7));
+        when(mockJobScheduler.getPendingJob(eq(MIN_CONTACTS_INDEXER_JOB_ID)))
+                .thenReturn(firstJobInfo);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ CONTACTS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
         ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
         verify(mockJobScheduler, times(2)).schedule(argumentCaptor.capture());
         List<JobInfo> jobInfos = argumentCaptor.getAllValues();
@@ -160,15 +187,24 @@
 
     @Test
     public void testScheduleFullUpdateJob_differentParams_isRescheduled() {
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContextWrapper, DEFAULT_USER_ID,
-                /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(7));
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ CONTACTS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
         ArgumentCaptor<JobInfo> firstJobInfoCaptor = ArgumentCaptor.forClass(JobInfo.class);
         verify(mockJobScheduler).schedule(firstJobInfoCaptor.capture());
         JobInfo firstJobInfo = firstJobInfoCaptor.getValue();
 
-        when(mockJobScheduler.getPendingJob(eq(MIN_INDEXER_JOB_ID))).thenReturn(firstJobInfo);
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContextWrapper, DEFAULT_USER_ID,
-                /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(30));
+        when(mockJobScheduler.getPendingJob(eq(MIN_CONTACTS_INDEXER_JOB_ID)))
+                .thenReturn(firstJobInfo);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ CONTACTS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(30));
         ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
         // Mockito.verify() counts the number of occurrences from the beginning of the test.
         // This verify() uses times(2) to also account for the call to JobScheduler.schedule() above
@@ -185,15 +221,24 @@
 
     @Test
     public void testScheduleFullUpdateJob_sameParams_isNotRescheduled() {
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContextWrapper, DEFAULT_USER_ID,
-                /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(7));
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ CONTACTS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
         ArgumentCaptor<JobInfo> argumentCaptor = ArgumentCaptor.forClass(JobInfo.class);
         verify(mockJobScheduler).schedule(argumentCaptor.capture());
         JobInfo firstJobInfo = argumentCaptor.getValue();
 
-        when(mockJobScheduler.getPendingJob(eq(MIN_INDEXER_JOB_ID))).thenReturn(firstJobInfo);
-        ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContextWrapper, DEFAULT_USER_ID,
-                /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(7));
+        when(mockJobScheduler.getPendingJob(eq(MIN_CONTACTS_INDEXER_JOB_ID)))
+                .thenReturn(firstJobInfo);
+        IndexerMaintenanceService.scheduleUpdateJob(
+                mContextWrapper,
+                DEFAULT_USER_HANDLE,
+                /* indexerType= */ CONTACTS_INDEXER,
+                /* periodic= */ true,
+                /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
         // Mockito.verify() counts the number of occurrences from the beginning of the test.
         // This verify() uses the default count of 1 (equivalent to times(1)) to account for the
         // call to JobScheduler.schedule() above where the first JobInfo is captured.
@@ -202,64 +247,67 @@
 
     @Test
     public void testDoFullUpdateForUser_withInitializedLocalService_isSuccessful() {
+        when(mParams.getExtras()).thenReturn(mExtras);
         ExtendedMockito.doReturn(Mockito.mock(ContactsIndexerManagerService.LocalService.class))
                 .when(() -> LocalManagerRegistry.getManager(
                         ContactsIndexerManagerService.LocalService.class));
-        boolean updateSucceeded = mContactsIndexerMaintenanceService
-                .doFullUpdateForUser(mContextWrapper, null, 0,
-                        new CancellationSignal());
+        boolean updateSucceeded =
+                mIndexerMaintenanceService.doUpdateForUser(
+                        mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
         assertThat(updateSucceeded).isTrue();
     }
 
     @Test
     public void testDoFullUpdateForUser_withUninitializedLocalService_failsGracefully() {
+        when(mParams.getExtras()).thenReturn(mExtras);
         ExtendedMockito.doReturn(null)
                 .when(() -> LocalManagerRegistry.getManager(
                         ContactsIndexerManagerService.LocalService.class));
-        boolean updateSucceeded = mContactsIndexerMaintenanceService
-                .doFullUpdateForUser(mContextWrapper, null, 0,
-                        new CancellationSignal());
+        boolean updateSucceeded =
+                mIndexerMaintenanceService.doUpdateForUser(
+                        mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
         assertThat(updateSucceeded).isFalse();
     }
 
     @Test
     public void testDoFullUpdateForUser_onEncounteringException_failsGracefully() {
+        when(mParams.getExtras()).thenReturn(mExtras);
         ContactsIndexerManagerService.LocalService mockService = Mockito.mock(
                 ContactsIndexerManagerService.LocalService.class);
-        doThrow(RuntimeException.class).when(mockService).doFullUpdateForUser(anyInt(), any());
+        doThrow(RuntimeException.class).when(mockService).doUpdateForUser(any(), any());
         ExtendedMockito.doReturn(mockService)
                 .when(() -> LocalManagerRegistry.getManager(
                         ContactsIndexerManagerService.LocalService.class));
 
-        boolean updateSucceeded = mContactsIndexerMaintenanceService
-                .doFullUpdateForUser(mContextWrapper, null, 0,
-                        new CancellationSignal());
+        boolean updateSucceeded =
+                mIndexerMaintenanceService.doUpdateForUser(
+                        mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
 
         assertThat(updateSucceeded).isFalse();
     }
 
     @Test
     public void testDoFullUpdateForUser_cancelsBackgroundJob_whenCiDisabled() {
+        when(mParams.getExtras()).thenReturn(mExtras);
         ExtendedMockito.doReturn(null)
                 .when(() -> LocalManagerRegistry.getManager(
                         ContactsIndexerManagerService.LocalService.class));
 
-        mContactsIndexerMaintenanceService
-                .doFullUpdateForUser(mContextWrapper, null, 0,
-                        new CancellationSignal());
+        mIndexerMaintenanceService.doUpdateForUser(
+                mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
 
-        verify(mockJobScheduler).cancel(MIN_INDEXER_JOB_ID);
+        verify(mockJobScheduler).cancel(MIN_CONTACTS_INDEXER_JOB_ID);
     }
 
     @Test
     public void testDoFullUpdateForUser_doesNotCancelBackgroundJob_whenCiEnabled() {
+        when(mParams.getExtras()).thenReturn(mExtras);
         ExtendedMockito.doReturn(Mockito.mock(ContactsIndexerManagerService.LocalService.class))
                 .when(() -> LocalManagerRegistry.getManager(
                         ContactsIndexerManagerService.LocalService.class));
 
-        mContactsIndexerMaintenanceService
-                .doFullUpdateForUser(mContextWrapper, null, 0,
-                        new CancellationSignal());
+        mIndexerMaintenanceService.doUpdateForUser(
+                mContextWrapper, mParams, DEFAULT_USER_HANDLE, new CancellationSignal());
 
         verifyZeroInteractions(mockJobScheduler);
     }
@@ -271,16 +319,20 @@
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         try {
             uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
-            ContactsIndexerMaintenanceService.scheduleFullUpdateJob(mContext, DEFAULT_USER_ID,
-                    /*periodic=*/ true, /*intervalMillis=*/ TimeUnit.DAYS.toMillis(7));
+            IndexerMaintenanceService.scheduleUpdateJob(
+                    mContext,
+                    DEFAULT_USER_HANDLE,
+                    /* indexerType= */ CONTACTS_INDEXER,
+                    /* periodic= */ true,
+                    /* intervalMillis= */ TimeUnit.DAYS.toMillis(7));
         } finally {
             uiAutomation.dropShellPermissionIdentity();
         }
         JobInfo jobInfo = getPendingFullUpdateJob(DEFAULT_USER_ID);
         assertThat(jobInfo).isNotNull();
 
-        ContactsIndexerMaintenanceService.cancelFullUpdateJobIfScheduled(mContext,
-                user.getUserHandle());
+        IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                mContext, user.getUserHandle(), CONTACTS_INDEXER);
 
         jobInfo = getPendingFullUpdateJob(DEFAULT_USER_ID);
         assertThat(jobInfo).isNull();
@@ -288,17 +340,17 @@
 
     @Test
     public void test_onStartJob_handlesExceptionGracefully() {
-        mContactsIndexerMaintenanceService.onStartJob(null);
+        mIndexerMaintenanceService.onStartJob(mParams);
     }
 
     @Test
     public void test_onStopJob_handlesExceptionGracefully() {
-        mContactsIndexerMaintenanceService.onStopJob(null);
+        mIndexerMaintenanceService.onStopJob(mParams);
     }
 
     @Nullable
     private JobInfo getPendingFullUpdateJob(@UserIdInt int userId) {
-        int jobId = MIN_INDEXER_JOB_ID + userId;
+        int jobId = MIN_CONTACTS_INDEXER_JOB_ID + userId;
         return mContext.getSystemService(JobScheduler.class).getPendingJob(jobId);
     }
 }
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerServiceTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerServiceTest.java
index cb1d4b0..159b77c 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerServiceTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerManagerServiceTest.java
@@ -19,7 +19,7 @@
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.Manifest.permission.RECEIVE_BOOT_COMPLETED;
 
-import static com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceService.MIN_INDEXER_JOB_ID;
+import static com.android.server.appsearch.contactsindexer.ContactsIndexerMaintenanceConfig.MIN_CONTACTS_INDEXER_JOB_ID;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -163,7 +163,7 @@
         fullUpdateLatch.await(30L, TimeUnit.SECONDS);
 
         // Verify that a periodic full-update job is scheduled still.
-        assertThat(getJobState(MIN_INDEXER_JOB_ID + userId)).contains("waiting");
+        assertThat(getJobState(MIN_CONTACTS_INDEXER_JOB_ID + userId)).contains("waiting");
 
         // Verify the stats for the ContactsIndexer. Two full updates are triggered at this
         // point, and the timestamps for 1st update must have been persisted.
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java
index 16daedc..129560f 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/ContactsIndexerUserInstanceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.appsearch.contactsindexer;
 
+import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.CONTACTS_INDEXER;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -54,6 +56,7 @@
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.modules.utils.testing.StaticMockFixture;
 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
+import com.android.server.appsearch.indexer.IndexerMaintenanceService;
 import com.android.server.appsearch.stats.AppSearchStatsLog;
 
 import org.junit.After;
@@ -361,9 +364,11 @@
         JobInfo mockJobInfo = mock(JobInfo.class);
         // getPendingJob() should return a non-null value to simulate the scenario where a
         // background job is already scheduled.
-        doReturn(mockJobInfo).when(mockJobScheduler).getPendingJob(
-                ContactsIndexerMaintenanceService.MIN_INDEXER_JOB_ID +
-                        mContext.getUser().getIdentifier());
+        doReturn(mockJobInfo)
+                .when(mockJobScheduler)
+                .getPendingJob(
+                        ContactsIndexerMaintenanceConfig.MIN_CONTACTS_INDEXER_JOB_ID
+                                + mContext.getUser().getIdentifier());
         mContext.setJobScheduler(mockJobScheduler);
         ContactsIndexerUserInstance instance = ContactsIndexerUserInstance.createInstance(
                 mContext, mContactsDir, mConfigForTest, mSingleThreadedExecutor);
@@ -745,8 +750,8 @@
         // adding it to update stats beforehand.
 
         // Cancel any existing jobs.
-        ContactsIndexerMaintenanceService.cancelFullUpdateJobIfScheduled(mContext,
-                mContext.getUser());
+        IndexerMaintenanceService.cancelUpdateJobIfScheduled(
+                mContext, mContext.getUser(), CONTACTS_INDEXER);
 
         JobScheduler mockJobScheduler = mock(JobScheduler.class);
         mContext.setJobScheduler(mockJobScheduler);
diff --git a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/EnterpriseContactsTest.java b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/EnterpriseContactsTest.java
index 53ab4e5..0910da5 100644
--- a/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/EnterpriseContactsTest.java
+++ b/testing/contactsindexertests/src/com/android/server/appsearch/contactsindexer/EnterpriseContactsTest.java
@@ -20,8 +20,8 @@
 import static android.app.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
 
 import static com.android.bedstead.harrier.UserType.WORK_PROFILE;
-import static com.android.bedstead.nene.permissions.CommonPermissions.INTERACT_ACROSS_USERS_FULL;
-import static com.android.bedstead.nene.permissions.CommonPermissions.WRITE_CONTACTS;
+import static com.android.bedstead.permissions.CommonPermissions.INTERACT_ACROSS_USERS_FULL;
+import static com.android.bedstead.permissions.CommonPermissions.WRITE_CONTACTS;
 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_ADDRESS;
 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_EMAIL;
 import static com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint.CONTACT_POINT_PROPERTY_LABEL;
@@ -81,8 +81,8 @@
 
 import com.android.bedstead.harrier.BedsteadJUnit4;
 import com.android.bedstead.harrier.DeviceState;
-import com.android.bedstead.harrier.annotations.EnsureHasPermission;
-import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile;
+import com.android.bedstead.permissions.annotations.EnsureHasPermission;
+import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile;
 import com.android.bedstead.nene.TestApis;
 import com.android.bedstead.remotedpc.RemoteDpc;
 import com.android.server.appsearch.contactsindexer.appsearchtypes.ContactPoint;
@@ -172,10 +172,12 @@
 
     @After
     public void tearDown() throws Exception {
-        // Wipe the data in AppSearchHelper.DATABASE_NAME.
-        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
-                .setForceOverride(true).build();
-        mDb.setSchemaAsync(setSchemaRequest).get();
+        if (mDb != null) {
+            // Wipe the data in AppSearchHelper.DATABASE_NAME.
+            SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                    .setForceOverride(true).build();
+            mDb.setSchemaAsync(setSchemaRequest).get();
+        }
     }
 
     private Person.Builder createPersonBuilder(String namespace, String id, String name) {
diff --git a/testing/coretests/Android.bp b/testing/coretests/Android.bp
index beb0bc1..adc4fe1 100644
--- a/testing/coretests/Android.bp
+++ b/testing/coretests/Android.bp
@@ -23,6 +23,7 @@
         "CtsAppSearchTestUtils",
         "androidx.test.ext.junit",
         "androidx.test.rules",
+        "appsearch_flags_java_lib",
         "junit",
         "testng",
         "truth",
diff --git a/testing/coretests/src/android/app/appsearch/AppSearchAttributionSourceUnitTest.java b/testing/coretests/src/android/app/appsearch/AppSearchAttributionSourceUnitTest.java
index 08de210..a1ea684 100644
--- a/testing/coretests/src/android/app/appsearch/AppSearchAttributionSourceUnitTest.java
+++ b/testing/coretests/src/android/app/appsearch/AppSearchAttributionSourceUnitTest.java
@@ -18,10 +18,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.appsearch.aidl.AppSearchAttributionSource;
-import android.content.Context;
+import static org.junit.Assert.assertThrows;
 
-import androidx.test.core.app.ApplicationProvider;
+import android.app.appsearch.aidl.AppSearchAttributionSource;
+
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
 
@@ -30,34 +31,60 @@
     @Test
     public void testSameAttributionSource() {
         AppSearchAttributionSource appSearchAttributionSource1 =
-                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1);
+                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1,
+                        /* callingPid= */ 1);
         AppSearchAttributionSource appSearchAttributionSource2 =
-                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1);
+                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1,
+                        /* callingPid= */ 1);
         assertThat(appSearchAttributionSource1.equals(appSearchAttributionSource2)).isTrue();
         assertThat(appSearchAttributionSource1.hashCode()).isEqualTo(
                 appSearchAttributionSource2.hashCode());
+        assertThat(appSearchAttributionSource1.getPid())
+                .isEqualTo(appSearchAttributionSource2.getPid());
     }
 
     @Test
     public void testDifferentAttributionSource() {
         AppSearchAttributionSource appSearchAttributionSource1 =
-                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1);
+                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1,
+                        /* callingPid= */ 1);
         AppSearchAttributionSource appSearchAttributionSource2 =
-                new AppSearchAttributionSource("testPackageName2", /* callingUid= */ 2);
+                new AppSearchAttributionSource("testPackageName2", /* callingUid= */ 2,
+                        /* callingPid= */ 1);
         assertThat(appSearchAttributionSource1.equals(appSearchAttributionSource2)).isFalse();
         assertThat(appSearchAttributionSource1.hashCode())
                 .isNotEqualTo(appSearchAttributionSource2.hashCode());
     }
 
     @Test
+    // Ideally this should never happen, but if AttributionSource does not have a package name we
+    // get a NullPointerException due to Objects.requireNonNull.
     public void testPackageNamesNull() {
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        new AppSearchAttributionSource(
+                                /* callingPackageName= */ null,
+                                /* callingUid= */ 1,
+                                /* callingPid= */ 1));
+    }
+
+    @Test
+    // We can only set and get pId in AttributionSource on U and above.
+    @SdkSuppress(minSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testDifferentAttributionSourcePid() {
         AppSearchAttributionSource appSearchAttributionSource1 =
-                new AppSearchAttributionSource(/* callingPackageName= */ null, /* callingUid= */ 1);
+                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1,
+                        /* callingPid= */ 1);
         AppSearchAttributionSource appSearchAttributionSource2 =
-                new AppSearchAttributionSource(/* callingPackageName= */ null, /* callingUid= */ 1);
-        assertThat(appSearchAttributionSource1.equals(appSearchAttributionSource2)).isTrue();
-        assertThat(appSearchAttributionSource1.hashCode())
-                .isEqualTo(appSearchAttributionSource2.hashCode());
+                new AppSearchAttributionSource("testPackageName1", /* callingUid= */ 1,
+                        /* callingPid= */ 2);
+        assertThat(appSearchAttributionSource1).isNotEqualTo(appSearchAttributionSource2);
+        // verify that AppSearchAttributionSource and AttributionSource contain different pId.
+        assertThat(appSearchAttributionSource1.getPid())
+                .isNotEqualTo(appSearchAttributionSource2.getPid());
+        assertThat(appSearchAttributionSource1.getAttributionSource().getPid())
+                .isNotEqualTo(appSearchAttributionSource2.getAttributionSource().getPid());
     }
 
 }
diff --git a/testing/coretests/src/android/app/appsearch/AppSearchSessionInternalTest.java b/testing/coretests/src/android/app/appsearch/AppSearchSessionInternalTest.java
index 8698830..1cf25f7 100644
--- a/testing/coretests/src/android/app/appsearch/AppSearchSessionInternalTest.java
+++ b/testing/coretests/src/android/app/appsearch/AppSearchSessionInternalTest.java
@@ -22,10 +22,10 @@
 
 import static org.junit.Assume.assumeTrue;
 
-import android.app.appsearch.testutil.AppSearchSessionShimImpl;
 import android.app.appsearch.AppSearchSchema.PropertyConfig;
 import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
 import android.app.appsearch.testutil.AppSearchEmail;
+import android.app.appsearch.testutil.AppSearchSessionShimImpl;
 import android.content.Context;
 
 import androidx.annotation.NonNull;
@@ -422,4 +422,336 @@
         assertThat(documents).hasSize(4);
         assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD);
     }
+
+    // TODO(b/290389974) Remove this override once GenericDocument#getParentTypes is unhidden.
+    @Override
+    @Test
+    public void testQuery_wildcardProjection_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("sender")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema textSchema =
+                new AppSearchSchema.Builder("Text")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("sender")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("sender")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(messageSchema, textSchema, emailSchema)
+                                .build())
+                .get();
+
+        // Index two child documents
+        GenericDocument text =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("sender", "Some sender")
+                        .setPropertyString("content", "Some note")
+                        .build();
+        GenericDocument email =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("sender", "Some sender")
+                        .setPropertyString("content", "Some note")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, text)
+                                .build()));
+
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "Some",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Message")
+                                .addProjection(
+                                        SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
+                                .addFilterProperties(
+                                        SearchSpec.SCHEMA_TYPE_WILDCARD,
+                                        ImmutableList.of("content"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString(PARENT_TYPES_SYNTHETIC_PROPERTY, "Message")
+                        .setPropertyString("sender", "Some sender")
+                        .build();
+        GenericDocument expectedEmail =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString(PARENT_TYPES_SYNTHETIC_PROPERTY, "Message")
+                        .setPropertyString("sender", "Some sender")
+                        .build();
+        assertThat(documents).containsExactly(expectedText, expectedEmail);
+    }
+
+    // TODO(b/290389974) Remove this override once GenericDocument#getParentTypes is unhidden.
+    @Override
+    @Test
+    public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema textSchema =
+                new AppSearchSchema.Builder("Text")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("carrier")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("attachment")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(messageSchema, textSchema, emailSchema)
+                                .build())
+                .get();
+
+        // Index two child documents
+        GenericDocument text =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("content", "Some note")
+                        .setPropertyString("carrier", "Network Inc")
+                        .build();
+        GenericDocument email =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("content", "Some note")
+                        .setPropertyString("attachment", "Network report")
+                        .build();
+
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, text)
+                                .build()));
+
+        // Both email and text would match for "Network", but only text should match as it is in the
+        // right property
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "Network",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Message")
+                                .addFilterProperties(
+                                        SearchSpec.SCHEMA_TYPE_WILDCARD,
+                                        ImmutableList.of("carrier"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString(PARENT_TYPES_SYNTHETIC_PROPERTY, "Message")
+                        .setPropertyString("content", "Some note")
+                        .setPropertyString("carrier", "Network Inc")
+                        .build();
+        assertThat(documents).containsExactly(expectedText);
+    }
+
+    // TODO(b/290389974) Remove this override once GenericDocument#getParentTypes is unhidden.
+    @Override
+    @Test
+    public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .build())
+                .get();
+
+        // Index two documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .setPropertyString("company", "Company")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc)
+                                .build()));
+
+        // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
+        // a schema filter for the "Person".
+        // This will be expanded to paths {"Person", ["name"]} and
+        // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
+        // polymorphism.
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterSchemas("Person")
+                                .addProjection("Person", ImmutableList.of("name"))
+                                .addProjection("Artist", ImmutableList.of("emailAddress"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The person document should have been returned with only the "name" property. The artist
+        // document should have been returned with all of its properties.
+        GenericDocument expectedPerson =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .build();
+        GenericDocument expectedArtist =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setPropertyString(PARENT_TYPES_SYNTHETIC_PROPERTY, "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+    }
 }
diff --git a/testing/coretests/src/android/app/appsearch/GlobalSearchSessionUnitTest.java b/testing/coretests/src/android/app/appsearch/GlobalSearchSessionUnitTest.java
index 8099114..a9556bd 100644
--- a/testing/coretests/src/android/app/appsearch/GlobalSearchSessionUnitTest.java
+++ b/testing/coretests/src/android/app/appsearch/GlobalSearchSessionUnitTest.java
@@ -17,6 +17,7 @@
 package android.app.appsearch;
 
 import android.app.appsearch.aidl.AppSearchAttributionSource;
+import android.app.appsearch.aidl.GetDocumentsAidlRequest;
 import android.app.appsearch.aidl.IAppSearchManager;
 import android.app.appsearch.exceptions.AppSearchException;
 import android.content.Context;
@@ -90,17 +91,18 @@
 
         String invalidPackageName = "not_this_package";
 
+
         service.getDocuments(
-                new AppSearchAttributionSource(invalidPackageName,
-                        android.os.Process.myUid()),
+                new GetDocumentsAidlRequest(
+                        new AppSearchAttributionSource(invalidPackageName,
+                                android.os.Process.myUid(), android.os.Process.myPid()),
                 /*targetPackageName=*/ mContext.getPackageName(),
                 /*databaseName*/ "testDb",
-                /*namespace=*/ "namespace",
-                /*ids=*/ ImmutableList.of("uri1"),
-                /*typePropertyPaths=*/ ImmutableMap.of(),
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds(/*ids=*/ ImmutableList.of("uri1")).build(),
                 android.os.Process.myUserHandle(),
                 SystemClock.elapsedRealtime(),
-                /*isForEnterprise=*/ false,
+                /*isForEnterprise=*/ false),
                 SearchSessionUtil.createGetDocumentCallback(mExecutor,
                         new BatchResultCallback<String, GenericDocument>() {
                             @Override
diff --git a/testing/coretests/src/android/app/appsearch/external/app/AppSearchSchemaInternalTest.java b/testing/coretests/src/android/app/appsearch/external/app/AppSearchSchemaInternalTest.java
deleted file mode 100644
index c355b77..0000000
--- a/testing/coretests/src/android/app/appsearch/external/app/AppSearchSchemaInternalTest.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright 2023 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 static com.google.common.truth.Truth.assertThat;
-
-import android.app.appsearch.testutil.AppSearchEmail;
-
-import org.junit.Test;
-
-import java.util.List;
-
-/** Tests for private APIs of {@link AppSearchSchema}. */
-public class AppSearchSchemaInternalTest {
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testParentTypes() {
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("EmailMessage")
-                        .addParentType("Email")
-                        .addParentType("Message")
-                        .build();
-        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDuplicateParentTypes() {
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("EmailMessage")
-                        .addParentType("Email")
-                        .addParentType("Message")
-                        .addParentType("Email")
-                        .build();
-        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_indexableNestedPropertyStrings() {
-        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedProperties("prop1", "prop2", "prop1.prop2")
-                        .build();
-        assertThat(documentPropertyConfig.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop2", "prop1.prop2");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_indexableNestedPropertyPropertyPaths() {
-        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedPropertyPaths(
-                                new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
-                        .build();
-        assertThat(documentPropertyConfig.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop1.prop2");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_indexableNestedPropertyProperty_duplicatePaths() {
-        AppSearchSchema.DocumentPropertyConfig documentPropertyConfig =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedPropertyPaths(
-                                new PropertyPath("prop1"), new PropertyPath("prop1.prop2"))
-                        .addIndexableNestedProperties("prop1")
-                        .build();
-        assertThat(documentPropertyConfig.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop1.prop2");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testDocumentPropertyConfig_reusingBuilderDoesNotAffectPreviouslyBuiltConfigs() {
-        AppSearchSchema.DocumentPropertyConfig.Builder builder =
-                new AppSearchSchema.DocumentPropertyConfig.Builder("property", "Schema")
-                        .addIndexableNestedProperties("prop1");
-        AppSearchSchema.DocumentPropertyConfig config1 = builder.build();
-        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-
-        builder.addIndexableNestedProperties("prop2");
-        AppSearchSchema.DocumentPropertyConfig config2 = builder.build();
-        assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
-        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-
-        builder.addIndexableNestedPropertyPaths(new PropertyPath("prop3"));
-        AppSearchSchema.DocumentPropertyConfig config3 = builder.build();
-        assertThat(config3.getIndexableNestedProperties())
-                .containsExactly("prop1", "prop2", "prop3");
-        assertThat(config2.getIndexableNestedProperties()).containsExactly("prop1", "prop2");
-        assertThat(config1.getIndexableNestedProperties()).containsExactly("prop1");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testPropertyConfig() {
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("Test")
-                        .addProperty(
-                                new AppSearchSchema.StringPropertyConfig.Builder("string")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.LongPropertyConfig.Builder("long")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setIndexingType(
-                                                AppSearchSchema.LongPropertyConfig
-                                                        .INDEXING_TYPE_NONE)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setIndexingType(
-                                                AppSearchSchema.LongPropertyConfig
-                                                        .INDEXING_TYPE_RANGE)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.DoublePropertyConfig.Builder("double")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                                "document1", AppSearchEmail.SCHEMA_TYPE)
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                        .setShouldIndexNestedProperties(true)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.DocumentPropertyConfig.Builder(
-                                                "document2", AppSearchEmail.SCHEMA_TYPE)
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                                        .setShouldIndexNestedProperties(false)
-                                        .addIndexableNestedProperties("path1", "path2", "path3")
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setJoinableValueType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .build())
-                        .addProperty(
-                                new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId2")
-                                        .setCardinality(
-                                                AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setJoinableValueType(
-                                                AppSearchSchema.StringPropertyConfig
-                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .build())
-                        .build();
-
-        assertThat(schema.getSchemaType()).isEqualTo("Test");
-        List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
-        assertThat(properties).hasSize(10);
-
-        assertThat(properties.get(0).getName()).isEqualTo("string");
-        assertThat(properties.get(0).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
-        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getIndexingType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS);
-        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN);
-
-        assertThat(properties.get(1).getName()).isEqualTo("long");
-        assertThat(properties.get(1).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
-                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE);
-
-        assertThat(properties.get(2).getName()).isEqualTo("indexableLong");
-        assertThat(properties.get(2).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(2)).getIndexingType())
-                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
-
-        assertThat(properties.get(3).getName()).isEqualTo("double");
-        assertThat(properties.get(3).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
-        assertThat(properties.get(3)).isInstanceOf(AppSearchSchema.DoublePropertyConfig.class);
-
-        assertThat(properties.get(4).getName()).isEqualTo("boolean");
-        assertThat(properties.get(4).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
-        assertThat(properties.get(4)).isInstanceOf(AppSearchSchema.BooleanPropertyConfig.class);
-
-        assertThat(properties.get(5).getName()).isEqualTo("bytes");
-        assertThat(properties.get(5).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(properties.get(5)).isInstanceOf(AppSearchSchema.BytesPropertyConfig.class);
-
-        assertThat(properties.get(6).getName()).isEqualTo("document1");
-        assertThat(properties.get(6).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
-        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(6)).getSchemaType())
-                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
-        assertThat(
-                        ((AppSearchSchema.DocumentPropertyConfig) properties.get(6))
-                                .shouldIndexNestedProperties())
-                .isEqualTo(true);
-
-        assertThat(properties.get(7).getName()).isEqualTo("document2");
-        assertThat(properties.get(7).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
-        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(7)).getSchemaType())
-                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
-        assertThat(
-                        ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
-                                .shouldIndexNestedProperties())
-                .isEqualTo(false);
-        assertThat(
-                        ((AppSearchSchema.DocumentPropertyConfig) properties.get(7))
-                                .getIndexableNestedProperties())
-                .containsExactly("path1", "path2", "path3");
-
-        assertThat(properties.get(8).getName()).isEqualTo("qualifiedId1");
-        assertThat(properties.get(8).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
-        assertThat(
-                        ((AppSearchSchema.StringPropertyConfig) properties.get(8))
-                                .getJoinableValueType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-
-        assertThat(properties.get(9).getName()).isEqualTo("qualifiedId2");
-        assertThat(properties.get(9).getCardinality())
-                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED);
-        assertThat(
-                        ((AppSearchSchema.StringPropertyConfig) properties.get(9))
-                                .getJoinableValueType())
-                .isEqualTo(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-    }
-}
diff --git a/testing/coretests/src/android/app/appsearch/external/app/AppSearchSessionInternalTestBase.java b/testing/coretests/src/android/app/appsearch/external/app/AppSearchSessionInternalTestBase.java
index 32d3e04..5c5b635 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/AppSearchSessionInternalTestBase.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/AppSearchSessionInternalTestBase.java
@@ -21,8 +21,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.annotation.NonNull;
@@ -80,7 +78,6 @@
     @Test
     public void testGetSchema_joinableValueType() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));
         AppSearchSchema inSchema =
                 new AppSearchSchema.Builder("Test")
                         .addProperty(
@@ -100,9 +97,6 @@
                                         .setJoinableValueType(
                                                 StringPropertyConfig
                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        // TODO(b/274157614): Export this to framework when we
-                                        //  can access hidden APIs.
-
                                         .build())
                         .build();
 
@@ -115,34 +109,6 @@
         assertThat(actual).containsExactlyElementsIn(request.getSchemas());
     }
 
-    // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
-    @Test
-    public void testGetSchema_deletionPropagation_unsupported() {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-        assumeFalse(
-                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DELETION_PROPAGATION));
-        AppSearchSchema schema =
-                new AppSearchSchema.Builder("Test")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("qualifiedIdDeletionPropagation")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setJoinableValueType(
-                                                StringPropertyConfig
-                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .setDeletionPropagation(true)
-                                        .build())
-                        .build();
-        SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(schema).build();
-        Exception e =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.setSchemaAsync(request).get());
-        assertThat(e.getMessage())
-                .isEqualTo(
-                        "Setting deletion propagation is not supported "
-                                + "on this AppSearch implementation.");
-    }
-
     @Test
     public void testQuery_typeFilterWithPolymorphism() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
@@ -344,6 +310,113 @@
     }
 
     @Test
+    public void testQuery_projectionWithPolymorphismAndSchemaFilter() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+
+        // Schema registration
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema artistSchema =
+                new AppSearchSchema.Builder("Artist")
+                        .addParentType("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("emailAddress")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("company")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchema)
+                                .addSchemas(artistSchema)
+                                .build())
+                .get();
+
+        // Index two documents
+        GenericDocument personDoc =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        GenericDocument artistDoc =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .setPropertyString("company", "Company")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(personDoc, artistDoc)
+                                .build()));
+
+        // Query with type property paths {"Person", ["name"]} and {"Artist", ["emailAddress"]}, and
+        // a schema filter for the "Person".
+        // This will be expanded to paths {"Person", ["name"]} and
+        // {"Artist", ["name", "emailAddress"]}, and filters for both "Person" and "Artist" via
+        // polymorphism.
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "Foo",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                                .addFilterSchemas("Person")
+                                .addProjection("Person", ImmutableList.of("name"))
+                                .addProjection("Artist", ImmutableList.of("emailAddress"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The person document should have been returned with only the "name" property. The artist
+        // document should have been returned with all of its properties.
+        GenericDocument expectedPerson =
+                new GenericDocument.Builder<>("namespace", "id1", "Person")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Person")
+                        .build();
+        GenericDocument expectedArtist =
+                new GenericDocument.Builder<>("namespace", "id2", "Artist")
+                        .setParentTypes(Collections.singletonList("Person"))
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("name", "Foo Artist")
+                        .setPropertyString("emailAddress", "[email protected]")
+                        .build();
+        assertThat(documents).containsExactly(expectedPerson, expectedArtist);
+    }
+
+    @Test
     public void testQuery_indexBasedOnParentTypePolymorphism() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
 
@@ -499,4 +572,225 @@
         assertThat(documents).hasSize(4);
         assertThat(documents).containsExactly(expectedDocA, expectedDocB, expectedDocC, docD);
     }
+
+    // TODO(b/336277840): Move this if setParentTypes becomes public
+    @Test
+    public void testQuery_wildcardProjection_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("sender")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema textSchema =
+                new AppSearchSchema.Builder("Text")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("sender")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("sender")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(messageSchema, textSchema, emailSchema)
+                                .build())
+                .get();
+
+        // Index two child documents
+        GenericDocument text =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("sender", "Some sender")
+                        .setPropertyString("content", "Some note")
+                        .build();
+        GenericDocument email =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("sender", "Some sender")
+                        .setPropertyString("content", "Some note")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, text)
+                                .build()));
+
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "Some",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Message")
+                                .addProjection(
+                                        SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("sender"))
+                                .addFilterProperties(
+                                        SearchSpec.SCHEMA_TYPE_WILDCARD,
+                                        ImmutableList.of("content"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setParentTypes(Collections.singletonList("Message"))
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("sender", "Some sender")
+                        .build();
+        GenericDocument expectedEmail =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setParentTypes(Collections.singletonList("Message"))
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("sender", "Some sender")
+                        .build();
+        assertThat(documents).containsExactly(expectedText, expectedEmail);
+    }
+
+    // TODO(b/336277840): Move this if setParentTypes becomes public
+    @Test
+    public void testQuery_wildcardFilterSchema_polymorphism() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
+        AppSearchSchema messageSchema =
+                new AppSearchSchema.Builder("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema textSchema =
+                new AppSearchSchema.Builder("Text")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("carrier")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addParentType("Message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("content")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("attachment")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(messageSchema, textSchema, emailSchema)
+                                .build())
+                .get();
+
+        // Index two child documents
+        GenericDocument text =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("content", "Some note")
+                        .setPropertyString("carrier", "Network Inc")
+                        .build();
+        GenericDocument email =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("content", "Some note")
+                        .setPropertyString("attachment", "Network report")
+                        .build();
+
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder()
+                                .addGenericDocuments(email, text)
+                                .build()));
+
+        // Both email and text would match for "Network", but only text should match as it is in the
+        // right property
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "Network",
+                        new SearchSpec.Builder()
+                                .addFilterSchemas("Message")
+                                .addFilterProperties(
+                                        SearchSpec.SCHEMA_TYPE_WILDCARD,
+                                        ImmutableList.of("carrier"))
+                                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // We specified the parent document in the filter schemas, but only indexed child documents.
+        // As we also specified a wildcard schema type projection, it should apply to the child docs
+        // The content property must not appear. Also emailNoContent should not appear as we are
+        // filter on the content property
+        GenericDocument expectedText =
+                new GenericDocument.Builder<>("namespace", "id1", "Text")
+                        .setParentTypes(Collections.singletonList("Message"))
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("content", "Some note")
+                        .setPropertyString("carrier", "Network Inc")
+                        .build();
+        assertThat(documents).containsExactly(expectedText);
+    }
 }
diff --git a/testing/coretests/src/android/app/appsearch/external/app/GenericDocumentInternalTest.java b/testing/coretests/src/android/app/appsearch/external/app/GenericDocumentInternalTest.java
index 07491e8..a5c49d7 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/GenericDocumentInternalTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/GenericDocumentInternalTest.java
@@ -45,7 +45,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
+        inParcel.writeParcelable(inDoc.getDocumentParcel(), /* parcelableFlags= */ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
@@ -87,7 +87,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
+        inParcel.writeParcelable(inDoc.getDocumentParcel(), /* parcelableFlags= */ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
diff --git a/testing/coretests/src/android/app/appsearch/external/app/InternalVisibilityConfigTest.java b/testing/coretests/src/android/app/appsearch/external/app/InternalVisibilityConfigTest.java
index 34b5953..d6bbd7b 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/InternalVisibilityConfigTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/InternalVisibilityConfigTest.java
@@ -81,7 +81,7 @@
                         .setSchemaTypeDisplayedBySystem("testSchema", false)
                         .setSchemaTypeVisibilityForPackage(
                                 "testSchema",
-                                /*visible=*/ true,
+                                /* visible= */ true,
                                 new PackageIdentifier("com.example.test", packageSha256Cert))
                         .setPubliclyVisibleSchema(
                                 "testSchema",
diff --git a/testing/coretests/src/android/app/appsearch/external/app/SearchResultInternalTest.java b/testing/coretests/src/android/app/appsearch/external/app/SearchResultInternalTest.java
index 9f8ae70..75fdedd 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/SearchResultInternalTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/SearchResultInternalTest.java
@@ -64,6 +64,23 @@
     }
 
     @Test
+    public void testSearchResultBuilderCopyConstructor_informationalRankingSignal() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        SearchResult searchResult =
+                new SearchResult.Builder("package", "database")
+                        .setGenericDocument(document)
+                        .setRankingSignal(1.23)
+                        .addInformationalRankingSignal(2)
+                        .addInformationalRankingSignal(3)
+                        .build();
+        SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+        assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());
+        assertThat(searchResultCopy.getInformationalRankingSignals())
+                .isEqualTo(searchResult.getInformationalRankingSignals());
+    }
+
+    @Test
     public void testSearchResultBuilder_clearJoinedResults() {
         GenericDocument document =
                 new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
diff --git a/testing/coretests/src/android/app/appsearch/external/app/SearchResultPageInternalTest.java b/testing/coretests/src/android/app/appsearch/external/app/SearchResultPageInternalTest.java
index d195184..aa180b1 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/SearchResultPageInternalTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/SearchResultPageInternalTest.java
@@ -36,7 +36,7 @@
                         new SearchResult.Builder("package2", "database2")
                                 .setGenericDocument(document)
                                 .build());
-        SearchResultPage searchResultPage = new SearchResultPage(/*nextPageToken=*/ 123, results);
+        SearchResultPage searchResultPage = new SearchResultPage(/* nextPageToken= */ 123, results);
         assertThat(searchResultPage.getNextPageToken()).isEqualTo(123);
         List<SearchResult> searchResults = searchResultPage.getResults();
         assertThat(searchResults).hasSize(2);
diff --git a/testing/coretests/src/android/app/appsearch/external/app/SearchSpecInternalTest.java b/testing/coretests/src/android/app/appsearch/external/app/SearchSpecInternalTest.java
index cdf8424..28ac20f 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/SearchSpecInternalTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/SearchSpecInternalTest.java
@@ -121,6 +121,47 @@
                 .isEqualTo(searchSpec.getSearchSourceLogTag());
     }
 
+    @Test
+    public void testSearchSpecBuilderCopyConstructor_embeddingSearch() {
+        EmbeddingVector embedding1 =
+                new EmbeddingVector(new float[] {1.1f, 2.2f, 3.3f}, "my_model_v1");
+        EmbeddingVector embedding2 =
+                new EmbeddingVector(new float[] {4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                        .addSearchEmbeddings(embedding1, embedding2)
+                        .build();
+
+        // Check that copy constructor works.
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getEnabledFeatures())
+                .containsExactlyElementsIn(searchSpec.getEnabledFeatures());
+        assertThat(searchSpecCopy.getDefaultEmbeddingSearchMetricType())
+                .isEqualTo(searchSpec.getDefaultEmbeddingSearchMetricType());
+        assertThat(searchSpecCopy.getSearchEmbeddings())
+                .containsExactlyElementsIn(searchSpec.getSearchEmbeddings());
+    }
+
+    @Test
+    public void testSearchSpecBuilderCopyConstructor_informationalRankingExpressions() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .setRankingStrategy("advancedExpression")
+                        .addInformationalRankingExpressions("this.relevanceScore()")
+                        .build();
+
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+        assertThat(searchSpecCopy.getAdvancedRankingExpression())
+                .isEqualTo(searchSpec.getAdvancedRankingExpression());
+        assertThat(searchSpecCopy.getInformationalRankingExpressions())
+                .isEqualTo(searchSpec.getInformationalRankingExpressions());
+    }
+
     // TODO(b/309826655): Flag guard this test.
     @Test
     public void testGetBundle_hasProperty() {
@@ -158,4 +199,59 @@
         assertThat(searchSpec3.getEnabledFeatures())
                 .containsExactly(Features.VERBATIM_SEARCH, Features.LIST_FILTER_QUERY_LANGUAGE);
     }
+
+    @Test
+    public void testGetEnabledFeatures_embeddingSearch() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .setNumericSearchEnabled(true)
+                        .setVerbatimSearchEnabled(true)
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setListFilterHasPropertyFunctionEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build();
+        assertThat(searchSpec.getEnabledFeatures())
+                .containsExactly(
+                        Features.NUMERIC_SEARCH,
+                        Features.VERBATIM_SEARCH,
+                        Features.LIST_FILTER_QUERY_LANGUAGE,
+                        Features.LIST_FILTER_HAS_PROPERTY_FUNCTION,
+                        FeatureConstants.EMBEDDING_SEARCH);
+
+        // Check that copy constructor works.
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getEnabledFeatures())
+                .containsExactly(
+                        Features.NUMERIC_SEARCH,
+                        Features.VERBATIM_SEARCH,
+                        Features.LIST_FILTER_QUERY_LANGUAGE,
+                        Features.LIST_FILTER_HAS_PROPERTY_FUNCTION,
+                        FeatureConstants.EMBEDDING_SEARCH);
+    }
+
+    @Test
+    public void testGetEnabledFeatures_tokenize() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .setNumericSearchEnabled(true)
+                        .setVerbatimSearchEnabled(true)
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setListFilterTokenizeFunctionEnabled(true)
+                        .build();
+        assertThat(searchSpec.getEnabledFeatures())
+                .containsExactly(
+                        Features.NUMERIC_SEARCH,
+                        Features.VERBATIM_SEARCH,
+                        Features.LIST_FILTER_QUERY_LANGUAGE,
+                        FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
+
+        // Check that copy constructor works.
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getEnabledFeatures())
+                .containsExactly(
+                        Features.NUMERIC_SEARCH,
+                        Features.VERBATIM_SEARCH,
+                        Features.LIST_FILTER_QUERY_LANGUAGE,
+                        FeatureConstants.LIST_FILTER_TOKENIZE_FUNCTION);
+    }
 }
diff --git a/testing/coretests/src/android/app/appsearch/external/app/SetSchemaResponseInternalTest.java b/testing/coretests/src/android/app/appsearch/external/app/SetSchemaResponseInternalTest.java
index 43fca6d..2a9e90c 100644
--- a/testing/coretests/src/android/app/appsearch/external/app/SetSchemaResponseInternalTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/app/SetSchemaResponseInternalTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
 import android.app.appsearch.AppSearchSchema.PropertyConfig;
 import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
 
@@ -90,7 +88,6 @@
                                         .setJoinableValueType(
                                                 StringPropertyConfig
                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .setDeletionPropagation(true)
                                         .build())
                         .build();
 
@@ -103,20 +100,5 @@
                 .isEqualTo(PropertyConfig.CARDINALITY_OPTIONAL);
         assertThat(((StringPropertyConfig) properties.get(0)).getJoinableValueType())
                 .isEqualTo(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID);
-        assertThat(((StringPropertyConfig) properties.get(0)).getDeletionPropagation())
-                .isEqualTo(true);
-    }
-
-    // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
-    @Test
-    public void testStringPropertyConfig_setJoinableProperty_deletePropagationError() {
-        final StringPropertyConfig.Builder builder =
-                new StringPropertyConfig.Builder("qualifiedId")
-                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                        .setDeletionPropagation(true);
-        IllegalStateException e = assertThrows(IllegalStateException.class, () -> builder.build());
-        assertThat(e)
-                .hasMessageThat()
-                .contains("Cannot set deletion propagation without setting a joinable value type");
     }
 }
diff --git a/testing/coretests/src/android/app/appsearch/external/flags/FlagsTest.java b/testing/coretests/src/android/app/appsearch/external/flags/FlagsTest.java
index e6b2045..53bb33a 100644
--- a/testing/coretests/src/android/app/appsearch/external/flags/FlagsTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/flags/FlagsTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.android.appsearch.flags.Flags;
+
 import org.junit.Test;
 
 public class FlagsTest {
@@ -91,4 +93,37 @@
         assertThat(Flags.FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION)
                 .isEqualTo("com.android.appsearch.flags.enable_enterprise_global_search_session");
     }
+
+    @Test
+    public void testFlagValue_enableResultDeniedAndResultRateLimited() {
+        assertThat(Flags.FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED)
+                .isEqualTo(
+                        "com.android.appsearch.flags.enable_result_denied_and_result_rate_limited");
+    }
+
+    @Test
+    public void testFlagValue_enableGetParentTypesAndIndexableNestedProperties() {
+        assertThat(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES)
+                .isEqualTo(
+                        "com.android.appsearch.flags"
+                                + ".enable_get_parent_types_and_indexable_nested_properties");
+    }
+
+    @Test
+    public void testFlagValue_enableSchemaEmbeddingPropertyConfig() {
+        assertThat(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+                .isEqualTo("com.android.appsearch.flags.enable_schema_embedding_property_config");
+    }
+
+    @Test
+    public void testFlagValue_enableListFilterTokenizeFunction() {
+        assertThat(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+                .isEqualTo("com.android.appsearch.flags.enable_list_filter_tokenize_function");
+    }
+
+    @Test
+    public void testFlagValue_enableInformationalRankingExpressions() {
+        assertThat(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+                .isEqualTo("com.android.appsearch.flags.enable_informational_ranking_expressions");
+    }
 }
diff --git a/testing/coretests/src/android/app/appsearch/external/safeparcel/GenericDocumentParcelTest.java b/testing/coretests/src/android/app/appsearch/external/safeparcel/GenericDocumentParcelTest.java
index b9a8aa6..a1ee712 100644
--- a/testing/coretests/src/android/app/appsearch/external/safeparcel/GenericDocumentParcelTest.java
+++ b/testing/coretests/src/android/app/appsearch/external/safeparcel/GenericDocumentParcelTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.app.appsearch.EmbeddingVector;
 import android.os.Parcel;
 
 import org.junit.Test;
@@ -38,6 +39,7 @@
         double[] doubleValues = {1.0, 2.0};
         boolean[] booleanValues = {true, false};
         byte[][] bytesValues = {new byte[1]};
+        EmbeddingVector[] embeddingValues = {new EmbeddingVector(new float[1], "my_model")};
         GenericDocumentParcel[] docValues = {
             (new GenericDocumentParcel.Builder("namespace", "id", "schemaType")).build()
         };
@@ -74,6 +76,12 @@
                 .isEqualTo(Arrays.copyOf(bytesValues, bytesValues.length));
         assertThat(
                         new PropertyParcel.Builder("name")
+                                .setEmbeddingValues(embeddingValues)
+                                .build()
+                                .getEmbeddingValues())
+                .isEqualTo(Arrays.copyOf(embeddingValues, embeddingValues.length));
+        assertThat(
+                        new PropertyParcel.Builder("name")
                                 .setDocumentValues(docValues)
                                 .build()
                                 .getDocumentValues())
@@ -99,19 +107,23 @@
     public void testGenericDocumentParcel_propertiesGeneratedCorrectly() {
         GenericDocumentParcel.Builder builder =
                 new GenericDocumentParcel.Builder(
-                        /*namespace=*/ "namespace", /*id=*/ "id", /*schemaType=*/ "schemaType");
+                        /* namespace= */ "namespace",
+                        /* id= */ "id",
+                        /* schemaType= */ "schemaType");
         long[] longArray = new long[] {1L, 2L, 3L};
         String[] stringArray = new String[] {"hello", "world", "!"};
-        builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
-        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        builder.putInPropertyMap(/* name= */ "longArray", /* values= */ longArray);
+        builder.putInPropertyMap(/* name= */ "stringArray", /* values= */ stringArray);
         GenericDocumentParcel genericDocumentParcel = builder.build();
 
         List<PropertyParcel> properties = genericDocumentParcel.getProperties();
         Map<String, PropertyParcel> propertyMap = genericDocumentParcel.getPropertyMap();
         PropertyParcel longArrayProperty =
-                new PropertyParcel.Builder(/*name=*/ "longArray").setLongValues(longArray).build();
+                new PropertyParcel.Builder(/* name= */ "longArray")
+                        .setLongValues(longArray)
+                        .build();
         PropertyParcel stringArrayProperty =
-                new PropertyParcel.Builder(/*name=*/ "stringArray")
+                new PropertyParcel.Builder(/* name= */ "stringArray")
                         .setStringValues(stringArray)
                         .build();
 
@@ -125,12 +137,14 @@
     public void testGenericDocumentParcel_buildFromAnotherDocumentParcelCorrectly() {
         GenericDocumentParcel.Builder builder =
                 new GenericDocumentParcel.Builder(
-                        /*namespace=*/ "namespace", /*id=*/ "id", /*schemaType=*/ "schemaType");
+                        /* namespace= */ "namespace",
+                        /* id= */ "id",
+                        /* schemaType= */ "schemaType");
         long[] longArray = new long[] {1L, 2L, 3L};
         String[] stringArray = new String[] {"hello", "world", "!"};
         List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
-        builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
-        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        builder.putInPropertyMap(/* name= */ "longArray", /* values= */ longArray);
+        builder.putInPropertyMap(/* name= */ "stringArray", /* values= */ stringArray);
         builder.setParentTypes(parentTypes);
         GenericDocumentParcel genericDocumentParcel = builder.build();
 
@@ -161,7 +175,9 @@
     public void testGenericDocumentParcelWithParentTypes() {
         GenericDocumentParcel.Builder builder =
                 new GenericDocumentParcel.Builder(
-                        /*namespace=*/ "namespace", /*id=*/ "id", /*schemaType=*/ "schemaType");
+                        /* namespace= */ "namespace",
+                        /* id= */ "id",
+                        /* schemaType= */ "schemaType");
         List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
 
         builder.setParentTypes(parentTypes);
@@ -174,23 +190,27 @@
     public void testGenericDocumentParcel_builderCanBeReused() {
         GenericDocumentParcel.Builder builder =
                 new GenericDocumentParcel.Builder(
-                        /*namespace=*/ "namespace", /*id=*/ "id", /*schemaType=*/ "schemaType");
+                        /* namespace= */ "namespace",
+                        /* id= */ "id",
+                        /* schemaType= */ "schemaType");
         long[] longArray = new long[] {1L, 2L, 3L};
         String[] stringArray = new String[] {"hello", "world", "!"};
         List<String> parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
-        builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
-        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        builder.putInPropertyMap(/* name= */ "longArray", /* values= */ longArray);
+        builder.putInPropertyMap(/* name= */ "stringArray", /* values= */ stringArray);
         builder.setParentTypes(parentTypes);
 
         GenericDocumentParcel genericDocumentParcel = builder.build();
         builder.setParentTypes(new ArrayList<>(Arrays.asList("parentType3", "parentType4")));
         builder.clearProperty("longArray");
-        builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ new String[] {""});
+        builder.putInPropertyMap(/* name= */ "stringArray", /* values= */ new String[] {""});
 
         PropertyParcel longArrayProperty =
-                new PropertyParcel.Builder(/*name=*/ "longArray").setLongValues(longArray).build();
+                new PropertyParcel.Builder(/* name= */ "longArray")
+                        .setLongValues(longArray)
+                        .build();
         PropertyParcel stringArrayProperty =
-                new PropertyParcel.Builder(/*name=*/ "stringArray")
+                new PropertyParcel.Builder(/* name= */ "stringArray")
                         .setStringValues(stringArray)
                         .build();
         assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
@@ -231,7 +251,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeParcelable(inDoc, /*flags=*/ 0);
+        inParcel.writeParcelable(inDoc, /* flags= */ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
diff --git a/testing/mockingservicestests/AndroidManifest.xml b/testing/mockingservicestests/AndroidManifest.xml
index 29a1c3f..e23ccb2 100644
--- a/testing/mockingservicestests/AndroidManifest.xml
+++ b/testing/mockingservicestests/AndroidManifest.xml
@@ -21,6 +21,9 @@
             android:label="AppSearchMockingServicesTests"
             android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
+        <service android:name="com.android.server.appsearch.AppSearchMaintenanceService"
+            android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="com.android.appsearch.mockingservicestests"
diff --git a/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchMaintenanceServiceTest.java b/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchMaintenanceServiceTest.java
new file mode 100644
index 0000000..e1732bf
--- /dev/null
+++ b/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchMaintenanceServiceTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2024 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 com.android.server.appsearch;
+
+import static android.Manifest.permission.RECEIVE_BOOT_COMPLETED;
+
+import static com.android.server.appsearch.AppSearchMaintenanceService.MIN_APPSEARCH_MAINTENANCE_JOB_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.Nullable;
+import android.app.UiAutomation;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.os.CancellationSignal;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.LocalManagerRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+public class AppSearchMaintenanceServiceTest {
+    private static final int DEFAULT_USER_ID = 0;
+
+    private Context mContext = ApplicationProvider.getApplicationContext();
+    private Context mContextWrapper;
+    private AppSearchMaintenanceService mAppSearchMaintenanceService;
+    private MockitoSession session;
+    @Mock
+    private JobScheduler mockJobScheduler;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContextWrapper = new ContextWrapper(mContext) {
+            @Override
+            @Nullable
+            public Object getSystemService(String name) {
+                if (Context.JOB_SCHEDULER_SERVICE.equals(name)) {
+                    return mockJobScheduler;
+                }
+                return getSystemService(name);
+            }
+        };
+        mAppSearchMaintenanceService = spy(new AppSearchMaintenanceService());
+        doNothing().when(mAppSearchMaintenanceService).jobFinished(any(), anyBoolean());
+        session = ExtendedMockito.mockitoSession().
+                mockStatic(LocalManagerRegistry.class).
+                startMocking();
+    }
+
+    @After
+    public void tearDown() {
+        session.finishMocking();
+    }
+
+    @Test
+    public void testScheduleFullPersistJob() {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        long intervalMillis = 37 * 60 * 1000; // 37 Min
+        try {
+            uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+            AppSearchMaintenanceService.scheduleFullyPersistJob(mContext, /*userId=*/123,
+                    intervalMillis);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+
+        int jobId = MIN_APPSEARCH_MAINTENANCE_JOB_ID + 123;
+        JobInfo jobInfo = mContext.getSystemService(JobScheduler.class).getPendingJob(jobId);
+        assertThat(jobInfo).isNotNull();
+        assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+        assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+        assertThat(jobInfo.isPersisted()).isTrue();
+        assertThat(jobInfo.isPeriodic()).isTrue();
+        assertThat(jobInfo.getIntervalMillis()).isEqualTo(intervalMillis);
+
+        int userId = jobInfo.getExtras().getInt("user_id", /*defaultValue=*/ -1);
+        assertThat(userId).isEqualTo(123);
+    }
+
+    @Test
+    public void testDoFullPersistForUser_withInitializedLocalService_isSuccessful() {
+        ExtendedMockito.doReturn(Mockito.mock(AppSearchManagerService.LocalService.class))
+                .when(() -> LocalManagerRegistry.getManager(
+                        AppSearchManagerService.LocalService.class));
+        assertThat(mAppSearchMaintenanceService
+                .doFullyPersistJobForUser(mContextWrapper, null, 0, new CancellationSignal()))
+                .isTrue();
+    }
+
+    @Test
+    public void testDoFullPersistForUser_withUninitializedLocalService_failsGracefully() {
+        ExtendedMockito.doReturn(null)
+                .when(() -> LocalManagerRegistry.getManager(
+                        AppSearchManagerService.LocalService.class));
+        assertThat(mAppSearchMaintenanceService
+                .doFullyPersistJobForUser(mContextWrapper, null, 0, new CancellationSignal()))
+                .isFalse();
+    }
+
+    @Test
+    public void testDoFullPersistForUser_onEncounteringException_failsGracefully()
+            throws Exception {
+        AppSearchManagerService.LocalService mockService = Mockito.mock(
+                AppSearchManagerService.LocalService.class);
+        doThrow(RuntimeException.class).when(mockService).doFullyPersistForUser(anyInt());
+        ExtendedMockito.doReturn(mockService)
+                .when(() -> LocalManagerRegistry.getManager(
+                        AppSearchManagerService.LocalService.class));
+
+        assertThat(mAppSearchMaintenanceService
+                .doFullyPersistJobForUser(mContextWrapper, null, 0, new CancellationSignal()))
+                .isFalse();
+    }
+
+    @Test
+    public void testDoFullPersistForUser_checkPendingJobIfNotInitialized() {
+        ExtendedMockito.doReturn(null)
+                .when(() -> LocalManagerRegistry.getManager(
+                        AppSearchManagerService.LocalService.class));
+
+        mAppSearchMaintenanceService.doFullyPersistJobForUser(
+                mContextWrapper, /*params=*/null, /*userId=*/123, new CancellationSignal());
+
+        // The server is not initialized, we should check and cancel any pending job. There will
+      // be a
+        // getPendingJob call to the job scheduler only. Since we haven't schedule any job.
+        verify(mockJobScheduler).getPendingJob(MIN_APPSEARCH_MAINTENANCE_JOB_ID + 123);
+    }
+
+    @Test
+    public void testCancelPendingFullPersistJob_succeeds() {
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity(RECEIVE_BOOT_COMPLETED);
+            AppSearchMaintenanceService.scheduleFullyPersistJob(mContext, DEFAULT_USER_ID,
+                    /*intervalMillis=*/456L);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+        int jobId = MIN_APPSEARCH_MAINTENANCE_JOB_ID + DEFAULT_USER_ID;
+        JobInfo jobInfo = mContext.getSystemService(JobScheduler.class).getPendingJob(jobId);
+        assertThat(jobInfo).isNotNull();
+
+        AppSearchMaintenanceService.cancelFullyPersistJobIfScheduled(mContext, DEFAULT_USER_ID);
+
+        jobInfo = mContext.getSystemService(JobScheduler.class).getPendingJob(jobId);
+        assertThat(jobInfo).isNull();
+    }
+
+    @Test
+    public void test_onStartJob_handlesExceptionGracefully() {
+        mAppSearchMaintenanceService.onStartJob(null);
+    }
+
+    @Test
+    public void test_onStopJob_handlesExceptionGracefully() {
+        mAppSearchMaintenanceService.onStopJob(null);
+    }
+}
diff --git a/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchManagerServiceTest.java b/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchManagerServiceTest.java
index ecccc59..4710537 100644
--- a/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchManagerServiceTest.java
+++ b/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchManagerServiceTest.java
@@ -16,15 +16,17 @@
 package com.android.server.appsearch;
 
 import static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
+import static android.app.appsearch.AppSearchResult.RESULT_DENIED;
+import static android.app.appsearch.AppSearchResult.RESULT_RATE_LIMITED;
 import static android.system.OsConstants.O_RDONLY;
 import static android.system.OsConstants.O_WRONLY;
 
 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_DENYLIST;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_API_COSTS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_ENABLED;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_DENYLIST;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_API_COSTS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_ENABLED;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -32,6 +34,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -44,14 +47,21 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.UiAutomation;
+import android.app.admin.DevicePolicyManager;
 import android.app.appsearch.AppSearchBatchResult;
 import android.app.appsearch.AppSearchEnvironmentFactory;
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.AppSearchSchema;
+import android.app.appsearch.AppSearchSchema.LongPropertyConfig;
+import android.app.appsearch.AppSearchSchema.PropertyConfig;
+import android.app.appsearch.AppSearchSchema.StringPropertyConfig;
 import android.app.appsearch.FrameworkAppSearchEnvironment;
 import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
 import android.app.appsearch.GetSchemaResponse;
 import android.app.appsearch.InternalSetSchemaResponse;
+import android.app.appsearch.RemoveByDocumentIdRequest;
+import android.app.appsearch.ReportUsageRequest;
 import android.app.appsearch.SearchResultPage;
 import android.app.appsearch.SearchSpec;
 import android.app.appsearch.SearchSuggestionSpec;
@@ -59,11 +69,14 @@
 import android.app.appsearch.aidl.AppSearchBatchResultParcel;
 import android.app.appsearch.aidl.AppSearchResultParcel;
 import android.app.appsearch.aidl.DocumentsParcel;
+import android.app.appsearch.aidl.ExecuteAppFunctionAidlRequest;
+import android.app.appsearch.aidl.GetDocumentsAidlRequest;
+import android.app.appsearch.aidl.GetNamespacesAidlRequest;
 import android.app.appsearch.aidl.GetNextPageAidlRequest;
 import android.app.appsearch.aidl.GetSchemaAidlRequest;
-import android.app.appsearch.aidl.GetNamespacesAidlRequest;
 import android.app.appsearch.aidl.GetStorageInfoAidlRequest;
 import android.app.appsearch.aidl.GlobalSearchAidlRequest;
+import android.app.appsearch.aidl.IAppFunctionService;
 import android.app.appsearch.aidl.IAppSearchBatchResultCallback;
 import android.app.appsearch.aidl.IAppSearchManager;
 import android.app.appsearch.aidl.IAppSearchObserverProxy;
@@ -76,13 +89,20 @@
 import android.app.appsearch.aidl.RegisterObserverCallbackAidlRequest;
 import android.app.appsearch.aidl.RemoveByDocumentIdAidlRequest;
 import android.app.appsearch.aidl.RemoveByQueryAidlRequest;
+import android.app.appsearch.aidl.ReportUsageAidlRequest;
 import android.app.appsearch.aidl.SearchAidlRequest;
 import android.app.appsearch.aidl.SearchSuggestionAidlRequest;
 import android.app.appsearch.aidl.SetSchemaAidlRequest;
 import android.app.appsearch.aidl.UnregisterObserverCallbackAidlRequest;
 import android.app.appsearch.aidl.WriteSearchResultsToFileAidlRequest;
+import android.app.appsearch.functions.AppFunctionManager;
+import android.app.appsearch.functions.ExecuteAppFunctionRequest;
+import android.app.appsearch.functions.ExecuteAppFunctionResponse;
+import android.app.appsearch.functions.ServiceCallHelper;
 import android.app.appsearch.observer.ObserverSpec;
+import android.app.appsearch.safeparcel.GenericDocumentParcel;
 import android.app.appsearch.stats.SchemaMigrationStats;
+import android.app.role.RoleManager;
 import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -90,12 +110,15 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
 import android.os.Handler;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.DeviceConfig;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -108,14 +131,18 @@
 import com.android.modules.utils.testing.TestableDeviceConfig;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.appsearch.external.localstorage.stats.CallStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchIntentStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchSessionStats;
 import com.android.server.appsearch.external.localstorage.stats.SearchStats;
 import com.android.server.appsearch.external.localstorage.stats.SetSchemaStats;
+import com.android.server.appsearch.external.localstorage.usagereporting.ClickActionGenericDocument;
+import com.android.server.appsearch.external.localstorage.usagereporting.SearchActionGenericDocument;
 import com.android.server.usage.StorageStatsManagerLocal;
 
-import libcore.io.IoBridge;
-
 import com.google.common.util.concurrent.SettableFuture;
 
+import libcore.io.IoBridge;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -125,9 +152,12 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
 
 public class AppSearchManagerServiceTest {
     private static final String DATABASE_NAME = "databaseName";
@@ -136,15 +166,11 @@
     private static final SearchSpec EMPTY_SEARCH_SPEC = new SearchSpec.Builder().build();
     // Mostly guarantees the logged estimated binder latency is positive and doesn't overflow
     private static final long BINDER_CALL_START_TIME = SystemClock.elapsedRealtime() - 1;
-    // TODO(b/279047435): use actual AppSearchResult.RESULT_DENIED constant after it's unhidden
-    private static final int RESULT_DENIED = 9;
-
-    // TODO(b/279047435): use actual AppSearchResult.RESULT_RATE_LIMITED constant after it's
-    //  unhidden
-    private static final int RESULT_RATE_LIMITED = 10;
     private static final String FOO_PACKAGE_NAME = "foo";
 
     private final MockServiceManager mMockServiceManager = new MockServiceManager();
+    private final RoleManager mRoleManager = mock(RoleManager.class);
+    private final DevicePolicyManager mDevicePolicyManager = mock(DevicePolicyManager.class);
 
     @Rule
     public ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder()
@@ -161,6 +187,9 @@
     private IAppSearchManager.Stub mAppSearchManagerServiceStub;
     private AppSearchUserInstance mUserInstance;
     private InternalAppSearchLogger mLogger;
+    private TestableServiceCallHelper mServiceCallHelper;
+
+    private int mCallingPid;
 
     @Before
     public void setUp() throws Exception {
@@ -170,6 +199,7 @@
         mContext = new ContextWrapper(context) {
             // Mock-able package manager for testing
             final PackageManager mPackageManager = spy(context.getPackageManager());
+            final UserManager mUserManager = spy(context.getSystemService(UserManager.class));
 
             @Override
             public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
@@ -193,6 +223,21 @@
             public PackageManager getPackageManager() {
                 return mPackageManager;
             }
+
+            @Nullable
+            @Override
+            public Object getSystemService(String name) {
+                if (Context.ROLE_SERVICE.equals(name)) {
+                    return mRoleManager;
+                }
+                if (Context.DEVICE_POLICY_SERVICE.equals(name)) {
+                    return mDevicePolicyManager;
+                }
+                if (Context.USER_SERVICE.equals(name)) {
+                    return mUserManager;
+                }
+                return super.getSystemService(name);
+            }
         };
 
         // Set a test environment that provides a temporary folder for AppSearch
@@ -206,11 +251,14 @@
                     }
                 });
 
-        // In AppSearchManagerService, FrameworkAppSearchConfig is a singleton. During tearDown for
+        setUpEnvironmentForAppFunction();
+        mServiceCallHelper = new TestableServiceCallHelper();
+
+        // In AppSearchManagerService, ServiceAppSearchConfig is a singleton. During tearDown for
         // TestableDeviceConfig, the propertyChangedListeners are removed. Therefore we have to set
         // a fresh config with listeners in setUp in order to set new properties.
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
         AppSearchComponentFactory.setConfigInstanceForTest(appSearchConfig);
 
         // Create the user instance and add a spy to its logger to verify logging
@@ -224,10 +272,11 @@
 
         // Start the service
         mAppSearchManagerService = new AppSearchManagerService(mContext,
-                new AppSearchModule.Lifecycle(mContext));
+                new AppSearchModule.Lifecycle(mContext), mServiceCallHelper);
         mAppSearchManagerService.onStart();
         mAppSearchManagerServiceStub = mMockServiceManager.mStubCaptor.getValue();
         assertThat(mAppSearchManagerServiceStub).isNotNull();
+        mCallingPid = android.os.Process.myPid();
     }
 
     @After
@@ -248,7 +297,8 @@
             TestResultCallback callback = new TestResultCallback();
             mAppSearchManagerServiceStub.initialize(
                     new InitializeAidlRequest(
-                            AppSearchAttributionSource.createAttributionSource(mContext),
+                            AppSearchAttributionSource.createAttributionSource(mContext,
+                                    mCallingPid),
                             testTargetUser, System.currentTimeMillis())
                     , callback);
             assertThat(callback.get().isSuccess()).isFalse();
@@ -265,7 +315,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.setSchema(
                 new SetSchemaAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
+                        DATABASE_NAME,
                 /* schemaBundles= */ Collections.emptyList(),
                 /* visibilityBundles= */ Collections.emptyList(), /* forceOverride= */ false,
                 /* schemaVersion= */ 0, mUserHandle, BINDER_CALL_START_TIME,
@@ -290,7 +341,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getSchema(
                 new GetSchemaAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                         BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                 callback);
@@ -304,7 +356,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getSchema(
                 new GetSchemaAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         otherPackageName, DATABASE_NAME, mUserHandle, BINDER_CALL_START_TIME,
                         /* isForEnterprise= */ false),
                 callback);
@@ -318,7 +371,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNamespaces(
                 new GetNamespacesAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         mUserHandle, BINDER_CALL_START_TIME),
                 callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
@@ -331,7 +385,8 @@
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.putDocuments(
                 new PutDocumentsAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         new DocumentsParcel(Collections.emptyList(), Collections.emptyList()),
                         mUserHandle, BINDER_CALL_START_TIME), callback);
         assertThat(callback.get()).isNull(); // null means there wasn't an error
@@ -341,13 +396,170 @@
     }
 
     @Test
+    public void testPutDocumentsStatsLogging_takenActions() throws Exception {
+        // Set SearchAction and ClickAction schemas.
+        List<AppSearchSchema> schemas =
+                Arrays.asList(
+                        new AppSearchSchema.Builder("builtin:SearchAction")
+                                .addProperty(
+                                        new LongPropertyConfig.Builder("actionType")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .addProperty(
+                                        new StringPropertyConfig.Builder("query")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .addProperty(
+                                        new LongPropertyConfig.Builder("fetchedResultCount")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .build(),
+                        new AppSearchSchema.Builder("builtin:ClickAction")
+                                .addProperty(
+                                        new LongPropertyConfig.Builder("actionType")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .addProperty(
+                                        new StringPropertyConfig.Builder("query")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .addProperty(
+                                        new LongPropertyConfig.Builder("resultRankInBlock")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .addProperty(
+                                        new LongPropertyConfig.Builder("resultRankGlobal")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .addProperty(
+                                        new LongPropertyConfig.Builder("timeStayOnResultMillis")
+                                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                                .build())
+                                .build());
+        InternalSetSchemaResponse internalSetSchemaResponse =
+                mUserInstance
+                        .getAppSearchImpl()
+                        .setSchema(
+                                mContext.getPackageName(),
+                                DATABASE_NAME,
+                                schemas,
+                                /* visibilityDocuments= */ Collections.emptyList(),
+                                /* forceOverride= */ false,
+                                /* version= */ 0,
+                                /* setSchemaStatsBuilder= */ null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Prepare search action and click action generic documents.
+        SearchActionGenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        ClickActionGenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickActionGenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .build();
+        SearchActionGenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(5000)
+                        .setQuery("test")
+                        .setFetchedResultCount(10)
+                        .build();
+        ClickActionGenericDocument clickAction3 =
+                new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(6000)
+                        .setQuery("test")
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(4)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        List<GenericDocumentParcel> takenActionGenericDocumentParcels =
+                Arrays.asList(
+                        GenericDocumentParcel.fromGenericDocument(searchAction1),
+                        GenericDocumentParcel.fromGenericDocument(clickAction1),
+                        GenericDocumentParcel.fromGenericDocument(clickAction2),
+                        GenericDocumentParcel.fromGenericDocument(searchAction2),
+                        GenericDocumentParcel.fromGenericDocument(clickAction3));
+
+        TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
+        mAppSearchManagerServiceStub.putDocuments(
+                new PutDocumentsAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
+                        DATABASE_NAME,
+                        new DocumentsParcel(
+                                Collections.emptyList(), takenActionGenericDocumentParcels),
+                        mUserHandle,
+                        BINDER_CALL_START_TIME),
+                callback);
+        assertThat(callback.get()).isNull(); // null means there wasn't an error
+        verifyCallStats(
+                mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_PUT_DOCUMENTS);
+
+        // Verify search sessions.
+        ArgumentCaptor<List<SearchSessionStats>> searchSessionsStatsCaptor =
+                ArgumentCaptor.forClass(List.class);
+        verify(mLogger, timeout(1000).times(1)).logStats(searchSessionsStatsCaptor.capture());
+        List<SearchSessionStats> searchSessionsStats = searchSessionsStatsCaptor.getValue();
+
+        assertThat(searchSessionsStats).hasSize(1);
+        assertThat(searchSessionsStats.get(0).getPackageName())
+                .isEqualTo(mContext.getPackageName());
+        assertThat(searchSessionsStats.get(0).getDatabase()).isEqualTo(DATABASE_NAME);
+
+        // Verify search intents.
+        List<SearchIntentStats> searchIntentsStats =
+                searchSessionsStats.get(0).getSearchIntentsStats();
+        assertThat(searchIntentsStats).hasSize(2);
+
+        assertThat(searchIntentsStats.get(0).getPackageName()).isEqualTo(mContext.getPackageName());
+        assertThat(searchIntentsStats.get(0).getDatabase()).isEqualTo(DATABASE_NAME);
+        assertThat(searchIntentsStats.get(0).getPrevQuery()).isNull();
+        assertThat(searchIntentsStats.get(0).getCurrQuery()).isEqualTo("tes");
+        assertThat(searchIntentsStats.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentsStats.get(0).getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentsStats.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentsStats.get(0).getClicksStats()).hasSize(2);
+
+        assertThat(searchIntentsStats.get(1).getPackageName()).isEqualTo(mContext.getPackageName());
+        assertThat(searchIntentsStats.get(1).getDatabase()).isEqualTo(DATABASE_NAME);
+        assertThat(searchIntentsStats.get(1).getPrevQuery()).isEqualTo("tes");
+        assertThat(searchIntentsStats.get(1).getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentsStats.get(1).getTimestampMillis()).isEqualTo(5000);
+        assertThat(searchIntentsStats.get(1).getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentsStats.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentsStats.get(1).getClicksStats()).hasSize(1);
+    }
+
+    @Test
     public void testLocalGetDocumentsStatsLogging() throws Exception {
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.getDocuments(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                mContext.getPackageName(), DATABASE_NAME, NAMESPACE,
-                /* ids= */ Collections.emptyList(), /* typePropertyPaths= */ Collections.emptyMap(),
-                mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false, callback);
+                new GetDocumentsAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        mContext.getPackageName(), DATABASE_NAME,
+                        new GetByDocumentIdRequest.Builder(NAMESPACE)
+                                .addIds(/* ids= */ Collections.emptyList())
+                                .build(),
+                mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
+                callback);
         assertThat(callback.get()).isNull(); // null means there wasn't an error
         verifyCallStats(mContext.getPackageName(), DATABASE_NAME,
                 CallStats.CALL_TYPE_GET_DOCUMENTS);
@@ -358,10 +570,15 @@
         String otherPackageName = mContext.getPackageName() + "foo";
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.getDocuments(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                otherPackageName, DATABASE_NAME, NAMESPACE, /* ids= */ Collections.emptyList(),
-                /* typePropertyPaths= */ Collections.emptyMap(), mUserHandle,
-                BINDER_CALL_START_TIME, /* isForEnterprise= */ false, callback);
+                new GetDocumentsAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        otherPackageName, DATABASE_NAME,
+                        new GetByDocumentIdRequest.Builder(NAMESPACE)
+                                .addIds(/* ids= */ Collections.emptyList())
+                                .build(),
+                       mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
+                callback);
         assertThat(callback.get()).isNull(); // null means there wasn't an error
         verifyCallStats(mContext.getPackageName(), DATABASE_NAME,
                 CallStats.CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID);
@@ -371,7 +588,8 @@
     public void testSearchStatsLogging() throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.search(
-                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext),
+                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext,
+                        mCallingPid),
                         DATABASE_NAME, /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                         BINDER_CALL_START_TIME), callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
@@ -383,7 +601,7 @@
     public void testGlobalSearchStatsLogging() throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.globalSearch(new GlobalSearchAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle, BINDER_CALL_START_TIME,
                 /* isForEnterprise= */ false), callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
@@ -395,7 +613,7 @@
     public void testLocalGetNextPageStatsLogging() throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 DATABASE_NAME, /* nextPageToken= */ 0,
                 AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                 BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
@@ -417,7 +635,7 @@
     public void testGlobalGetNextPageStatsLogging() throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* databaseName= */ null, /* nextPageToken= */ 0,
                 AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                 BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
@@ -437,7 +655,7 @@
     @Test
     public void testInvalidateNextPageTokenStatsLogging() throws Exception {
         mAppSearchManagerServiceStub.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* nextPageToken= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                 /* isForEnterprise= */ false));
         verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN);
@@ -450,7 +668,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.writeSearchResultsToFile(
                 new WriteSearchResultsToFileAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         new ParcelFileDescriptor(fd), /* searchExpression= */ "", EMPTY_SEARCH_SPEC,
                         mUserHandle, BINDER_CALL_START_TIME), callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
@@ -464,7 +683,8 @@
         FileDescriptor fd = IoBridge.open(tempFile.getPath(), O_RDONLY);
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.putDocumentsFromFile(new PutDocumentsFromFileAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
+                DATABASE_NAME,
                 new ParcelFileDescriptor(fd), mUserHandle,
                 new SchemaMigrationStats.Builder(mContext.getPackageName(), DATABASE_NAME).build(),
                 /* totalLatencyStartTimeMillis= */ 0, BINDER_CALL_START_TIME), callback);
@@ -488,7 +708,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.searchSuggestion(
                 new SearchSuggestionAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         DATABASE_NAME, /* suggestionQueryExpression= */ "foo", searchSuggestionSpec,
                         mUserHandle, BINDER_CALL_START_TIME),
                 callback);
@@ -503,10 +724,15 @@
         setUpTestDocument(mContext.getPackageName(), DATABASE_NAME, NAMESPACE, ID);
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.reportUsage(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                mContext.getPackageName(), DATABASE_NAME, NAMESPACE, ID,
-                /* usageTimestampMillis= */ 0, /* systemUsage= */ false, mUserHandle,
-                BINDER_CALL_START_TIME, callback);
+                new ReportUsageAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        mContext.getPackageName(), DATABASE_NAME,
+                        new ReportUsageRequest.Builder(NAMESPACE, ID)
+                                .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
+                                .build(),
+                        /* systemUsage= */ false, mUserHandle, BINDER_CALL_START_TIME),
+                callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
         verifyCallStats(mContext.getPackageName(), DATABASE_NAME, CallStats.CALL_TYPE_REPORT_USAGE);
         removeTestSchema(mContext.getPackageName(), DATABASE_NAME);
@@ -522,9 +748,15 @@
             setUpTestDocument(otherPackageName, DATABASE_NAME, NAMESPACE, ID);
             TestResultCallback callback = new TestResultCallback();
             mAppSearchManagerServiceStub.reportUsage(
-                    AppSearchAttributionSource.createAttributionSource(mContext),
-                    otherPackageName, DATABASE_NAME, NAMESPACE, ID, /* usageTimestampMillis= */ 0,
-                    /* systemUsage= */ true, mUserHandle, BINDER_CALL_START_TIME, callback);
+                    new ReportUsageAidlRequest(
+                            AppSearchAttributionSource.createAttributionSource(mContext,
+                                    mCallingPid),
+                            otherPackageName, DATABASE_NAME,
+                            new ReportUsageRequest.Builder(NAMESPACE, ID)
+                                    .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
+                                    .build(),
+                            /* systemUsage= */ true, mUserHandle, BINDER_CALL_START_TIME),
+                    callback);
             assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
             verifyCallStats(mContext.getPackageName(), DATABASE_NAME,
                     CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE);
@@ -539,8 +771,13 @@
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.removeByDocumentId(
                 new RemoveByDocumentIdAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
-                        DATABASE_NAME, NAMESPACE, /* ids= */ Collections.emptyList(), mUserHandle,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        DATABASE_NAME,
+                        new RemoveByDocumentIdRequest.Builder(NAMESPACE)
+                                .addIds(/* ids= */ Collections.emptyList())
+                                .build(),
+                        mUserHandle,
                         BINDER_CALL_START_TIME),
                 callback);
         assertThat(callback.get()).isNull(); // null means there wasn't an error
@@ -553,7 +790,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.removeByQuery(
                 new RemoveByQueryAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         /* queryExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                         BINDER_CALL_START_TIME),
                 callback);
@@ -567,7 +805,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getStorageInfo(
                 new GetStorageInfoAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         mUserHandle, BINDER_CALL_START_TIME),
                 callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
@@ -579,7 +818,8 @@
     public void testPersistToDiskStatsLogging() throws Exception {
         mAppSearchManagerServiceStub.persistToDisk(
                 new PersistToDiskAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), mUserHandle,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), mUserHandle,
                         BINDER_CALL_START_TIME));
         verifyCallStats(mContext.getPackageName(), CallStats.CALL_TYPE_FLUSH);
     }
@@ -589,7 +829,8 @@
         AppSearchResultParcel<Void> resultParcel =
                 mAppSearchManagerServiceStub.registerObserverCallback(
                         new RegisterObserverCallbackAidlRequest(
-                                AppSearchAttributionSource.createAttributionSource(mContext),
+                                AppSearchAttributionSource.createAttributionSource(mContext,
+                                        mCallingPid),
                                 mContext.getPackageName(),
                                 new ObserverSpec.Builder().build(),
                                 mUserHandle, BINDER_CALL_START_TIME),
@@ -614,7 +855,8 @@
         AppSearchResultParcel<Void> resultParcel =
                 mAppSearchManagerServiceStub.unregisterObserverCallback(
                         new UnregisterObserverCallbackAidlRequest(
-                                AppSearchAttributionSource.createAttributionSource(mContext),
+                                AppSearchAttributionSource.createAttributionSource(mContext,
+                                        mCallingPid),
                                 mContext.getPackageName(), mUserHandle,
                                 BINDER_CALL_START_TIME),
                         new IAppSearchObserverProxy.Stub() {
@@ -639,7 +881,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.initialize(
                 new InitializeAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), mUserHandle,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), mUserHandle,
                         BINDER_CALL_START_TIME),
                 callback);
         assertThat(callback.get().getResultCode()).isEqualTo(AppSearchResult.RESULT_OK);
@@ -673,7 +916,7 @@
                         + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                         + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                         + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
-                        + "initialize";
+                        + "initialize,executeAppFunction";
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, denylistString, false);
         // We expect all local calls (pkg+db) and global calls (pkg only) to be denied since the
@@ -692,7 +935,7 @@
                         + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                         + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                         + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
-                        + "initialize";
+                        + "initialize,executeAppFunction";
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, denylistString, false);
         // We expect none of the local calls (pkg+db) and global calls (pkg only) to be denied since
@@ -712,7 +955,7 @@
                         + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                         + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                         + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
-                        + "initialize";
+                        + "initialize,executeAppFunction";
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, denylistString, false);
         // We expect only the local calls (pkg+db) to be denied since the denylist specifies a
@@ -742,7 +985,7 @@
                         + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                         + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                         + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
-                        + "initialize";
+                        + "initialize,executeAppFunction";
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, denylistString, false);
         verifyLocalCallsResults(AppSearchResult.RESULT_OK);
@@ -759,7 +1002,7 @@
                         + "localPutDocumentsFromFile,localSearchSuggestion,globalReportUsage,"
                         + "localReportUsage,localRemoveByDocumentId,localRemoveBySearch,"
                         + "localGetStorageInfo,flush,globalRegisterObserverCallback,"
-                        + "globalUnregisterObserverCallback,initialize";
+                        + "globalUnregisterObserverCallback,initialize,executeAppFunction";
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, denylistString, false);
 
@@ -769,7 +1012,8 @@
         // Add mocking to spy'd package manager to return current uid for package foo
         // This is necessary to pass call verification using a different package name
         PackageManager spyPackageManager = mContext.getPackageManager();
-        int uid = AppSearchAttributionSource.createAttributionSource(mContext).getUid();
+        int uid = AppSearchAttributionSource.createAttributionSource(mContext,
+                mCallingPid).getUid();
         doReturn(uid).when(spyPackageManager).getPackageUid(FOO_PACKAGE_NAME, /* flags= */ 0);
         // Specifically grant permission for report system usage to package foo
         doReturn(PackageManager.PERMISSION_GRANTED).when(spyPackageManager).checkPermission(
@@ -792,7 +1036,7 @@
 
         // Confirm that we're using a different package name
         assertThat(mContext.getPackageName()).isEqualTo(FOO_PACKAGE_NAME);
-        assertThat(AppSearchAttributionSource.createAttributionSource(mContext)
+        assertThat(AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid)
                 .getPackageName()).isEqualTo(FOO_PACKAGE_NAME);
 
         verifyLocalCallsResults(RESULT_DENIED);
@@ -809,7 +1053,7 @@
                         + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                         + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                         + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
-                        + "initialize";
+                        + "initialize,executeAppFunction";
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, denylistString, false);
         verifyLocalCallsResults(AppSearchResult.RESULT_OK);
@@ -849,6 +1093,46 @@
         verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyInitializeResult(AppSearchResult.RESULT_OK);
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_OK);
+    }
+
+    @Test
+    public void testAppSearchRateLimit_rateLimitOn_allApis() throws Exception {
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_RATE_LIMIT_ENABLED, Boolean.toString(true), false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY, Integer.toString(1), false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE,
+                Float.toString(0.8f),
+                false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_RATE_LIMIT_API_COSTS,
+                "localSetSchema:5;globalGetSchema:5;localGetSchema:5;localGetNamespaces:5;"
+                        + "localPutDocuments:5;globalGetDocuments:5;localGetDocuments:5;"
+                        + "localSearch:5;globalSearch:5;globalGetNextPage:5;localGetNextPage:5;"
+                        + "invalidateNextPageToken:5;localWriteSearchResultsToFile:5;"
+                        + "localPutDocumentsFromFile:5;localSearchSuggestion:5;"
+                        + "globalReportUsage:5;localReportUsage:5;localRemoveByDocumentId:5;"
+                        + "localRemoveBySearch:5;localGetStorageInfo:5;flush:5;"
+                        + "executeAppFunction:5",
+                false);
+
+        verifySetSchemaResult(RESULT_RATE_LIMITED);
+        verifyLocalGetSchemaResult(RESULT_RATE_LIMITED);
+        verifySearchResult(RESULT_RATE_LIMITED);
+        verifyPutDocumentsResult(RESULT_RATE_LIMITED);
+        verifyLocalGetDocumentsResult(RESULT_RATE_LIMITED);
+        verifyLocalGetNextPageResult(RESULT_RATE_LIMITED);
+        verifyGlobalGetDocumentsResult(RESULT_RATE_LIMITED);
+        verifyGlobalSearchResult(RESULT_RATE_LIMITED);
+        verifyGlobalGetNextPageResult(RESULT_RATE_LIMITED);
+        verifyInvalidateNextPageTokenResult(RESULT_RATE_LIMITED);
+        verifyGlobalReportUsageResult(RESULT_RATE_LIMITED);
+        verifyPersistToDiskResult(RESULT_RATE_LIMITED);
+        verifyExecuteAppFunctionCallbackResult(RESULT_RATE_LIMITED);
+
+        // initialize, registerObserver and unregisterObserver do not have rate limit.
     }
 
     @Test
@@ -906,6 +1190,7 @@
         verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyInitializeResult(AppSearchResult.RESULT_OK);
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_OK);
     }
 
     @Test
@@ -956,6 +1241,7 @@
         verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyInitializeResult(AppSearchResult.RESULT_OK);
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_OK);
 
         // All calls should be fine after switching rate limiting to off
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
@@ -999,6 +1285,7 @@
         verifyRegisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyUnregisterObserverCallbackResult(AppSearchResult.RESULT_OK);
         verifyInitializeResult(AppSearchResult.RESULT_OK);
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_OK);
     }
 
     @Test
@@ -1118,7 +1405,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getSchema(
                 new GetSchemaAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                         BINDER_CALL_START_TIME, /* isForEnterprise= */ true),
                 callback);
@@ -1136,10 +1424,15 @@
         // unlocked the enterprise user for our local instance of AppSearchManagerService
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.getDocuments(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                mContext.getPackageName(), DATABASE_NAME, NAMESPACE,
-                /* ids= */ Collections.emptyList(), /* typePropertyPaths= */ Collections.emptyMap(),
-                mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ true, callback);
+                new GetDocumentsAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                mContext.getPackageName(), DATABASE_NAME, new GetByDocumentIdRequest.Builder(
+                        NAMESPACE)
+                        .addIds(/* ids= */ Collections.emptyList())
+                        .build(),
+                mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ true),
+                callback);
         assertThat(callback.get()).isNull(); // null means there wasn't an error
         assertThat(callback.getBatchResult().getAll()).isEmpty();
         // No CallStats logged since we returned early
@@ -1152,7 +1445,7 @@
         // unlocked the enterprise user for our local instance of AppSearchManagerService
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.globalSearch(new GlobalSearchAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle, BINDER_CALL_START_TIME,
                 /* isForEnterprise= */ true), callback);
         AppSearchResult<SearchResultPage> result =
@@ -1169,7 +1462,7 @@
         // unlocked the enterprise user for our local instance of AppSearchManagerService
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* databaseName= */ null,/* nextPageToken= */ 0,
                 AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                 BINDER_CALL_START_TIME, /* isForEnterprise= */ true), callback);
@@ -1186,13 +1479,98 @@
         // Even on devices with an enterprise user, this test will run properly, since we haven't
         // unlocked the enterprise user for our local instance of AppSearchManagerService
         mAppSearchManagerServiceStub.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* nextPageToken= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                 /* isForEnterprise= */ true));
         // No CallStats logged since we returned early
         verify(mLogger, timeout(1000).times(0)).logStats(any(CallStats.class));
     }
 
+    @Test
+    public void executeAppFunction_success() throws Exception {
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_OK);
+    }
+
+    @Test
+    public void executeAppFunction_callerNoPermission() throws Exception {
+        doReturn(List.of())
+                .when(mRoleManager).getRoleHoldersAsUser(
+                        AppSearchManagerService.SYSTEM_UI_INTELLIGENCE, mUserHandle);
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_SECURITY_ERROR);
+    }
+
+    @Test
+    public void executeAppFunction_cannotResolveService() throws Exception {
+        PackageManager spyPackageManager = mContext.getPackageManager();
+        doReturn(null).when(spyPackageManager).resolveService(any(Intent.class), eq(0));
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void executeAppFunction_serviceNotPermissionProtected() throws Exception {
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.packageName = FOO_PACKAGE_NAME;
+        serviceInfo.name = ".MyAppFunctionService";
+        ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.serviceInfo = serviceInfo;
+        PackageManager spyPackageManager = mContext.getPackageManager();
+        doReturn(resolveInfo).when(spyPackageManager).resolveService(any(Intent.class), eq(0));
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_NOT_FOUND);
+    }
+
+    @Test
+    public void executeAppFunction_bindServiceReturnsFalse() throws Exception {
+        mServiceCallHelper.setBindServiceResult(false);
+        mServiceCallHelper.setOnRunServiceCallListener((callback) -> {});
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void executeAppFunction_failedToConnectService() throws Exception {
+        mServiceCallHelper.setOnRunServiceCallListener(
+                ServiceCallHelper.RunServiceCallCallback::onFailedToConnect);
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void executeAppFunction_serviceConnectionTimeout() throws Exception {
+        mServiceCallHelper.setOnRunServiceCallListener(
+                ServiceCallHelper.RunServiceCallCallback::onTimedOut);
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_TIMED_OUT);
+    }
+
+    @Test
+    public void executeAppFunction_executeAppFunctionReturnsFailure() throws Exception {
+        mServiceCallHelper.setOnRunServiceCallListener(
+                (callback) -> callback.onServiceConnected(new TestableAppFunctionService(
+                        AppSearchResult.newFailedResult(AppSearchResult.RESULT_INVALID_ARGUMENT,
+                                "errorMessage")), () -> {
+                }));
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_INVALID_ARGUMENT);
+    }
+
+    @Test
+    public void executeAppFunction_hasDeviceOwner_fail() throws Exception {
+        doReturn(true).when(mDevicePolicyManager).isDeviceManaged();
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_SECURITY_ERROR);
+    }
+
+    @Test
+    public void executeAppFunction_fromManagedProfile_fail() throws Exception {
+        UserManager spyUserManager = mContext.getSystemService(UserManager.class);
+        doReturn(true).when(spyUserManager).isManagedProfile(mUserHandle.getIdentifier());
+
+        verifyExecuteAppFunctionCallbackResult(AppSearchResult.RESULT_SECURITY_ERROR);
+    }
+
     private void verifyLocalCallsResults(int resultCode) throws Exception {
         // These APIs are local calls since they specify a database. If the API specifies a target
         // package, then the target package matches the calling package
@@ -1226,6 +1604,7 @@
         verifyRegisterObserverCallbackResult(resultCode);
         verifyUnregisterObserverCallbackResult(resultCode);
         verifyInitializeResult(resultCode);
+        verifyExecuteAppFunctionCallbackResult(resultCode);
     }
 
     private void verifySetSchemaResult(int resultCode) throws Exception {
@@ -1233,7 +1612,7 @@
         mAppSearchManagerServiceStub.setSchema(
                 new SetSchemaAidlRequest(
                 AppSearchAttributionSource
-                    .createAttributionSource(mContext), DATABASE_NAME,
+                    .createAttributionSource(mContext, mCallingPid), DATABASE_NAME,
                 /* schemaBundles= */ Collections.emptyList(),
                 /* visibilityBundles= */ Collections.emptyList(), /* forceOverride= */ false,
                 /* schemaVersion= */ 0, mUserHandle, BINDER_CALL_START_TIME,
@@ -1246,7 +1625,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getSchema(
                 new GetSchemaAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         mContext.getPackageName(), DATABASE_NAME, mUserHandle,
                         BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
                 callback);
@@ -1258,7 +1638,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getSchema(
                 new GetSchemaAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         otherPackageName, DATABASE_NAME, mUserHandle, BINDER_CALL_START_TIME,
                         /* isForEnterprise= */ false),
                 callback);
@@ -1269,7 +1650,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNamespaces(
                 new GetNamespacesAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         mUserHandle, BINDER_CALL_START_TIME),
                 callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_NAMESPACES, callback.get());
@@ -1279,7 +1661,8 @@
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.putDocuments(
                 new PutDocumentsAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         new DocumentsParcel(Collections.emptyList(), Collections.emptyList()),
                         mUserHandle, BINDER_CALL_START_TIME), callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_PUT_DOCUMENTS, callback.get());
@@ -1288,10 +1671,15 @@
     private void verifyLocalGetDocumentsResult(int resultCode) throws Exception {
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.getDocuments(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                mContext.getPackageName(), DATABASE_NAME, NAMESPACE,
-                /* ids= */ Collections.emptyList(), /* typePropertyPaths= */ Collections.emptyMap(),
-                mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false, callback);
+                new GetDocumentsAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        mContext.getPackageName(), DATABASE_NAME,
+                        new GetByDocumentIdRequest.Builder(NAMESPACE)
+                                .addIds(/* ids= */ Collections.emptyList())
+                                .build(),
+                        mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
+                callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_DOCUMENTS, callback.get());
     }
 
@@ -1299,17 +1687,23 @@
         String otherPackageName = mContext.getPackageName() + "foo";
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.getDocuments(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                otherPackageName, DATABASE_NAME, NAMESPACE, /* ids= */ Collections.emptyList(),
-                /* typePropertyPaths= */ Collections.emptyMap(), mUserHandle,
-                BINDER_CALL_START_TIME, /* isForEnterprise= */ false, callback);
+                new GetDocumentsAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        otherPackageName, DATABASE_NAME,
+                        new GetByDocumentIdRequest.Builder(NAMESPACE)
+                                .addIds(/* ids= */ Collections.emptyList())
+                                .build(),
+                        mUserHandle, BINDER_CALL_START_TIME, /* isForEnterprise= */ false),
+                callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_GLOBAL_GET_DOCUMENT_BY_ID, callback.get());
     }
 
     private void verifySearchResult(int resultCode) throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.search(
-                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext),
+                new SearchAidlRequest(AppSearchAttributionSource.createAttributionSource(mContext,
+                        mCallingPid),
                         DATABASE_NAME,/* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                         BINDER_CALL_START_TIME), callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_SEARCH, callback.get());
@@ -1318,7 +1712,7 @@
     private void verifyGlobalSearchResult(int resultCode) throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.globalSearch(new GlobalSearchAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* searchExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle, BINDER_CALL_START_TIME,
                 /* isForEnterprise= */ false), callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_GLOBAL_SEARCH, callback.get());
@@ -1327,7 +1721,8 @@
     private void verifyLocalGetNextPageResult(int resultCode) throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
+                DATABASE_NAME,
                 /* nextPageToken= */ 0,
                 AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                 BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
@@ -1337,7 +1732,7 @@
     private void verifyGlobalGetNextPageResult(int resultCode) throws Exception {
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getNextPage(new GetNextPageAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* databaseName= */ null, /* nextPageToken= */ 0,
                 AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID, mUserHandle,
                 BINDER_CALL_START_TIME, /* isForEnterprise= */ false), callback);
@@ -1346,7 +1741,7 @@
 
     private void verifyInvalidateNextPageTokenResult(int resultCode) throws Exception {
         mAppSearchManagerServiceStub.invalidateNextPageToken(new InvalidateNextPageTokenAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext),
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
                 /* nextPageToken= */ 0, mUserHandle, BINDER_CALL_START_TIME,
                 /* isForEnterprise= */ false));
         verifyCallResult(resultCode, CallStats.CALL_TYPE_INVALIDATE_NEXT_PAGE_TOKEN, /* result= */
@@ -1359,7 +1754,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.writeSearchResultsToFile(
                 new WriteSearchResultsToFileAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         new ParcelFileDescriptor(fd), /* searchExpression= */ "", EMPTY_SEARCH_SPEC,
                         mUserHandle, BINDER_CALL_START_TIME), callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_WRITE_SEARCH_RESULTS_TO_FILE,
@@ -1371,7 +1767,8 @@
         FileDescriptor fd = IoBridge.open(tempFile.getPath(), O_RDONLY);
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.putDocumentsFromFile(new PutDocumentsFromFileAidlRequest(
-                AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                AppSearchAttributionSource.createAttributionSource(mContext, mCallingPid),
+                DATABASE_NAME,
                 new ParcelFileDescriptor(fd), mUserHandle,
                 new SchemaMigrationStats.Builder(mContext.getPackageName(), DATABASE_NAME).build(),
                 /* totalLatencyStartTimeMillis= */ 0, BINDER_CALL_START_TIME), callback);
@@ -1384,7 +1781,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.searchSuggestion(
                 new SearchSuggestionAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
                         DATABASE_NAME, /* suggestionQueryExpression= */ "foo", searchSuggestionSpec,
                         mUserHandle, BINDER_CALL_START_TIME),
                 callback);
@@ -1396,10 +1794,15 @@
         setUpTestDocument(mContext.getPackageName(), DATABASE_NAME, NAMESPACE, ID);
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.reportUsage(
-                AppSearchAttributionSource.createAttributionSource(mContext),
-                mContext.getPackageName(), DATABASE_NAME, NAMESPACE, ID,
-                /* usageTimestampMillis= */ 0, /* systemUsage= */ false, mUserHandle,
-                BINDER_CALL_START_TIME, callback);
+                new ReportUsageAidlRequest(
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                mContext.getPackageName(), DATABASE_NAME,
+                        new ReportUsageRequest.Builder(NAMESPACE, ID)
+                                .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
+                                .build(),
+                        /* systemUsage= */ false, mUserHandle, BINDER_CALL_START_TIME),
+                callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_REPORT_USAGE, callback.get());
         removeTestSchema(mContext.getPackageName(), DATABASE_NAME);
     }
@@ -1413,9 +1816,15 @@
             setUpTestDocument(otherPackageName, DATABASE_NAME, NAMESPACE, ID);
             TestResultCallback callback = new TestResultCallback();
             mAppSearchManagerServiceStub.reportUsage(
-                    AppSearchAttributionSource.createAttributionSource(mContext),
-                    otherPackageName, DATABASE_NAME, NAMESPACE, ID, /* usageTimestampMillis= */ 0,
-                    /* systemUsage= */ true, mUserHandle, BINDER_CALL_START_TIME, callback);
+                    new ReportUsageAidlRequest(
+                            AppSearchAttributionSource.createAttributionSource(mContext,
+                                    mCallingPid),
+                            otherPackageName, DATABASE_NAME,
+                            new ReportUsageRequest.Builder(NAMESPACE, ID)
+                                    .setUsageTimestampMillis(/* usageTimestampMillis= */ 0)
+                                    .build(),
+                    /* systemUsage= */ true, mUserHandle, BINDER_CALL_START_TIME),
+                    callback);
             verifyCallResult(resultCode, CallStats.CALL_TYPE_REPORT_SYSTEM_USAGE, callback.get());
             removeTestSchema(otherPackageName, DATABASE_NAME);
         } finally {
@@ -1427,8 +1836,13 @@
         TestBatchResultErrorCallback callback = new TestBatchResultErrorCallback();
         mAppSearchManagerServiceStub.removeByDocumentId(
                 new RemoveByDocumentIdAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext),
-                        DATABASE_NAME, NAMESPACE, /* ids= */ Collections.emptyList(), mUserHandle,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        DATABASE_NAME,
+                        new RemoveByDocumentIdRequest.Builder(NAMESPACE)
+                                .addIds(/* ids= */ Collections.emptyList())
+                                .build(),
+                        mUserHandle,
                         BINDER_CALL_START_TIME),
                 callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID, callback.get());
@@ -1438,7 +1852,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.removeByQuery(
                 new RemoveByQueryAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         /* queryExpression= */ "", EMPTY_SEARCH_SPEC, mUserHandle,
                         BINDER_CALL_START_TIME),
                 callback);
@@ -1450,7 +1865,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.getStorageInfo(
                 new GetStorageInfoAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), DATABASE_NAME,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), DATABASE_NAME,
                         mUserHandle, BINDER_CALL_START_TIME),
                 callback);
         verifyCallResult(resultCode, CallStats.CALL_TYPE_GET_STORAGE_INFO, callback.get());
@@ -1459,7 +1875,8 @@
     private void verifyPersistToDiskResult(int resultCode) throws Exception {
         mAppSearchManagerServiceStub.persistToDisk(
                 new PersistToDiskAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), mUserHandle,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), mUserHandle,
                         BINDER_CALL_START_TIME));
         verifyCallResult(resultCode, CallStats.CALL_TYPE_FLUSH, /* result= */ null);
     }
@@ -1468,7 +1885,8 @@
         AppSearchResultParcel<Void> resultParcel =
                 mAppSearchManagerServiceStub.registerObserverCallback(
                         new RegisterObserverCallbackAidlRequest(
-                                AppSearchAttributionSource.createAttributionSource(mContext),
+                                AppSearchAttributionSource.createAttributionSource(mContext,
+                                        mCallingPid),
                                 mContext.getPackageName(),
                                 new ObserverSpec.Builder().build(),
                                 mUserHandle,
@@ -1489,11 +1907,30 @@
                 resultParcel.getResult());
     }
 
+    private void verifyExecuteAppFunctionCallbackResult(int resultCode) throws Exception {
+        TestResultCallback callback = new TestResultCallback();
+        mAppSearchManagerServiceStub.executeAppFunction(
+                new ExecuteAppFunctionAidlRequest(
+                        new ExecuteAppFunctionRequest.Builder(
+                                FOO_PACKAGE_NAME, "function"
+                        ).build(),
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid),
+                        mUserHandle,
+                        BINDER_CALL_START_TIME
+                ),
+                callback
+        );
+
+        verifyCallResult(resultCode, CallStats.CALL_TYPE_EXECUTE_APP_FUNCTION, callback.get());
+    }
+
     private void verifyUnregisterObserverCallbackResult(int resultCode) throws Exception {
         AppSearchResultParcel<Void> resultParcel =
                 mAppSearchManagerServiceStub.unregisterObserverCallback(
                         new UnregisterObserverCallbackAidlRequest(
-                                AppSearchAttributionSource.createAttributionSource(mContext),
+                                AppSearchAttributionSource.createAttributionSource(mContext,
+                                        mCallingPid),
                                 mContext.getPackageName(), mUserHandle,
                                 BINDER_CALL_START_TIME),
                         new IAppSearchObserverProxy.Stub() {
@@ -1516,7 +1953,8 @@
         TestResultCallback callback = new TestResultCallback();
         mAppSearchManagerServiceStub.initialize(
                 new InitializeAidlRequest(
-                        AppSearchAttributionSource.createAttributionSource(mContext), mUserHandle,
+                        AppSearchAttributionSource.createAttributionSource(mContext,
+                                mCallingPid), mUserHandle,
                         BINDER_CALL_START_TIME),
                 callback);
         if (resultCode == RESULT_DENIED) {
@@ -1573,6 +2011,26 @@
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
     }
 
+    private void setUpEnvironmentForAppFunction() {
+        doReturn(Arrays.asList(mContext.getPackageName()))
+                .when(mRoleManager).getRoleHoldersAsUser(
+                        AppSearchManagerService.SYSTEM_UI_INTELLIGENCE, mUserHandle);
+
+        // FOO_PACKAGE implemented an AppFunctionService.
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.packageName = FOO_PACKAGE_NAME;
+        serviceInfo.name = ".MyAppFunctionService";
+        serviceInfo.permission = AppFunctionManager.PERMISSION_BIND_APP_FUNCTION_SERVICE;
+        ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.serviceInfo = serviceInfo;
+        PackageManager spyPackageManager = mContext.getPackageManager();
+        doReturn(resolveInfo).when(spyPackageManager).resolveService(any(Intent.class), eq(0));
+
+        doReturn(false).when(mDevicePolicyManager).isDeviceManaged();
+        UserManager spyUserManager = mContext.getSystemService(UserManager.class);
+        doReturn(false).when(spyUserManager).isManagedProfile(mUserHandle.getIdentifier());
+    }
+
     private static class MockServiceManager implements StaticMockFixture {
         ArgumentCaptor<IAppSearchManager.Stub> mStubCaptor = ArgumentCaptor.forClass(
                 IAppSearchManager.Stub.class);
@@ -1639,4 +2097,70 @@
             return batchFuture.get();
         }
     }
+
+    /**
+     * Testable helper for {@link ServiceCallHelper}, defaults to the happy case, i.e. successful
+     * service connection and
+     * {@link android.app.appsearch.functions.AppFunctionService#onExecuteFunction} always
+     * returns a successful result. Includes methods to customize connection behavior.
+     */
+    private static class TestableServiceCallHelper implements
+            ServiceCallHelper<IAppFunctionService> {
+        private Consumer<RunServiceCallCallback<IAppFunctionService>> mOnRunServiceCallListener =
+                (callback) -> callback.onServiceConnected(new TestableAppFunctionService(
+                        AppSearchResult.newSuccessfulResult(
+                                new ExecuteAppFunctionResponse.Builder().build())), () -> {
+                });
+        private boolean mBindServiceResult = true;
+
+        /**
+         * Replaces the default service connection behavior. Use this in tests to simulate
+         * different connection results (e.g., failures).
+         */
+        public void setOnRunServiceCallListener(
+                @NonNull Consumer<RunServiceCallCallback<IAppFunctionService>> listener) {
+            mOnRunServiceCallListener = Objects.requireNonNull(listener);
+        }
+
+        /** Sets the result of {@link #runServiceCall} (defaults to {@code true}). */
+        public void setBindServiceResult(boolean bindResult) {
+            mBindServiceResult = bindResult;
+        }
+
+        @Override
+        public boolean runServiceCall(@NonNull Intent intent, int bindFlags,
+                long timeoutInMillis, @NonNull UserHandle userHandle,
+                @NonNull RunServiceCallCallback<IAppFunctionService> callback) {
+            mOnRunServiceCallListener.accept(callback);
+            return mBindServiceResult;
+        }
+    }
+
+    /**
+     * A testable implementation of {@link IAppFunctionService.Stub} for you to customize the
+     * result of {@link #executeAppFunction}.
+     */
+    private static class TestableAppFunctionService extends IAppFunctionService.Stub {
+        private final AppSearchResult<ExecuteAppFunctionResponse> mResult;
+
+        /**
+         * @param result the result to return in {@link #executeAppFunction}.
+         */
+        public TestableAppFunctionService(
+                @NonNull AppSearchResult<ExecuteAppFunctionResponse> result) {
+            mResult = Objects.requireNonNull(result);
+        }
+
+        @Override
+        public void executeAppFunction(
+                ExecuteAppFunctionRequest executeAppFunctionRequest,
+                IAppSearchResultCallback callback) throws RemoteException {
+            if (mResult.isSuccess()) {
+                callback.onResult(AppSearchResultParcel.fromExecuteAppFunctionResponse(
+                        mResult.getResultValue()));
+            } else {
+                callback.onResult(AppSearchResultParcel.fromFailedResult(mResult));
+            }
+        }
+    }
 }
diff --git a/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchModuleTest.java b/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchModuleTest.java
new file mode 100644
index 0000000..c0e3682
--- /dev/null
+++ b/testing/mockingservicestests/src/com/android/server/appsearch/AppSearchModuleTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.appsearch;
+
+import static com.android.server.appsearch.appsindexer.AppsIndexerConfig.DEFAULT_APPS_INDEXER_ENABLED;
+import static com.android.server.appsearch.contactsindexer.ContactsIndexerConfig.DEFAULT_CONTACTS_INDEXER_ENABLED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.SystemService.TargetUser;
+import com.android.server.appsearch.AppSearchModule.Lifecycle;
+import com.android.server.appsearch.appsindexer.AppsIndexerConfig;
+import com.android.server.appsearch.appsindexer.AppsIndexerManagerService;
+import com.android.server.appsearch.contactsindexer.ContactsIndexerConfig;
+import com.android.server.appsearch.contactsindexer.ContactsIndexerManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class AppSearchModuleTest {
+    private static final String NAMESPACE_APPSEARCH = "appsearch";
+    private static final String KEY_CONTACTS_INDEXER_ENABLED = "contacts_indexer_enabled";
+    private static final String KEY_APPS_INDEXER_ENABLED = "apps_indexer_enabled";
+
+    private final ContactsIndexerManagerService mContactsIndexerService =
+            mock(ContactsIndexerManagerService.class);
+    private final AppsIndexerManagerService mAppsIndexerService =
+            mock(AppsIndexerManagerService.class);
+    private final AppSearchManagerService mAppSearchService = mock(AppSearchManagerService.class);
+
+    private TargetUser mUser;
+    private Lifecycle mLifecycle;
+    private MockitoSession mMockitoSession;
+
+    @Before
+    public void setUp() {
+        mMockitoSession =
+                ExtendedMockito.mockitoSession()
+                        .mockStatic(DeviceConfig.class)
+                        .strictness(Strictness.LENIENT)
+                        .startMocking();
+        Context context = ApplicationProvider.getApplicationContext();
+        UserInfo userInfo = new UserInfo(context.getUserId(), "default", 0);
+        mUser = new TargetUser(userInfo);
+
+        mLifecycle =
+                new Lifecycle(context) {
+                    @NonNull
+                    @Override
+                    AppsIndexerManagerService createAppsIndexerManagerService(
+                            @NonNull Context context, @NonNull AppsIndexerConfig config) {
+                        return mAppsIndexerService;
+                    }
+
+                    @NonNull
+                    @Override
+                    ContactsIndexerManagerService createContactsIndexerManagerService(
+                            @NonNull Context context, @NonNull ContactsIndexerConfig config) {
+                        return mContactsIndexerService;
+                    }
+
+                    @NonNull
+                    @Override
+                    AppSearchManagerService createAppSearchManagerService(
+                            @NonNull Context context, @NonNull Lifecycle lifecycle) {
+                        return mAppSearchService;
+                    }
+                };
+
+        ExtendedMockito.doReturn(true)
+                .when(
+                        () ->
+                                DeviceConfig.getBoolean(
+                                        NAMESPACE_APPSEARCH,
+                                        KEY_CONTACTS_INDEXER_ENABLED,
+                                        DEFAULT_CONTACTS_INDEXER_ENABLED));
+        ExtendedMockito.doReturn(true)
+                .when(
+                        () ->
+                                DeviceConfig.getBoolean(
+                                        NAMESPACE_APPSEARCH,
+                                        KEY_APPS_INDEXER_ENABLED,
+                                        DEFAULT_APPS_INDEXER_ENABLED));
+    }
+
+    @After
+    public void tearDown() {
+        mMockitoSession.finishMocking();
+    }
+
+    @Test
+    public void testBothIndexersEnabled() {
+        mLifecycle.onStart();
+        assertThat(mLifecycle.mAppsIndexerManagerService).isNotNull();
+        assertThat(mLifecycle.mContactsIndexerManagerService).isNotNull();
+
+        mLifecycle.onUserUnlocking(mUser);
+        verify(mContactsIndexerService).onUserUnlocking(mUser);
+        verify(mAppsIndexerService).onUserUnlocking(mUser);
+
+        mLifecycle.onUserStopping(mUser);
+        verify(mContactsIndexerService).onUserStopping(mUser);
+        verify(mAppsIndexerService).onUserStopping(mUser);
+    }
+
+    @Test
+    public void testContactsIndexerDisabled() {
+        ExtendedMockito.doReturn(false)
+                .when(
+                        () ->
+                                DeviceConfig.getBoolean(
+                                        NAMESPACE_APPSEARCH,
+                                        KEY_CONTACTS_INDEXER_ENABLED,
+                                        DEFAULT_CONTACTS_INDEXER_ENABLED));
+
+        mLifecycle.onStart();
+        assertNull(mLifecycle.mContactsIndexerManagerService);
+
+        mLifecycle.onUserUnlocking(mUser);
+        verify(mAppsIndexerService).onUserUnlocking(mUser);
+        assertNull(mLifecycle.mContactsIndexerManagerService);
+
+        mLifecycle.onUserStopping(mUser);
+        verify(mAppsIndexerService).onUserStopping(mUser);
+        assertNull(mLifecycle.mContactsIndexerManagerService);
+    }
+
+    @Test
+    public void testAppsIndexerDisabled() {
+        ExtendedMockito.doReturn(false)
+                .when(
+                        () ->
+                                DeviceConfig.getBoolean(
+                                        NAMESPACE_APPSEARCH,
+                                        KEY_APPS_INDEXER_ENABLED,
+                                        DEFAULT_APPS_INDEXER_ENABLED));
+
+        mLifecycle.onStart();
+        assertNull(mLifecycle.mAppsIndexerManagerService);
+
+        mLifecycle.onUserUnlocking(mUser);
+        verify(mContactsIndexerService).onUserUnlocking(mUser);
+        assertNull(mLifecycle.mAppsIndexerManagerService);
+
+        mLifecycle.onUserStopping(mUser);
+        verify(mContactsIndexerService).onUserStopping(mUser);
+        assertNull(mLifecycle.mAppsIndexerManagerService);
+    }
+
+    @Test
+    public void testServicesSetToNullWhenDisabled() {
+        ExtendedMockito.doReturn(false)
+                .when(
+                        () ->
+                                DeviceConfig.getBoolean(
+                                        NAMESPACE_APPSEARCH,
+                                        KEY_CONTACTS_INDEXER_ENABLED,
+                                        DEFAULT_CONTACTS_INDEXER_ENABLED));
+        ExtendedMockito.doReturn(false)
+                .when(
+                        () ->
+                                DeviceConfig.getBoolean(
+                                        NAMESPACE_APPSEARCH,
+                                        KEY_APPS_INDEXER_ENABLED,
+                                        DEFAULT_APPS_INDEXER_ENABLED));
+
+        mLifecycle.onStart();
+        assertNull(mLifecycle.mContactsIndexerManagerService);
+        assertNull(mLifecycle.mAppsIndexerManagerService);
+
+        mLifecycle.onUserUnlocking(mUser);
+        assertNull(mLifecycle.mContactsIndexerManagerService);
+        assertNull(mLifecycle.mAppsIndexerManagerService);
+
+        mLifecycle.onUserStopping(mUser);
+        assertNull(mLifecycle.mContactsIndexerManagerService);
+        assertNull(mLifecycle.mAppsIndexerManagerService);
+    }
+
+    @Test
+    public void testIndexerOnStart_clearsService() {
+        // Setup AppsIndexerManagerService to throw an error on start
+        doThrow(new RuntimeException("Apps indexer exception")).when(mAppsIndexerService).onStart();
+
+        mLifecycle.onStart();
+        assertThat(mLifecycle.mAppsIndexerManagerService).isNull();
+        assertThat(mLifecycle.mContactsIndexerManagerService).isNotNull();
+
+        //  Setup ContactsIndexerManagerService to throw an error on start
+        doNothing().when(mAppsIndexerService).onStart();
+        doThrow(new RuntimeException("Contacts indexer exception"))
+                .when(mContactsIndexerService)
+                .onStart();
+
+        mLifecycle.onStart();
+        assertThat(mLifecycle.mAppsIndexerManagerService).isNotNull();
+        assertThat(mLifecycle.mContactsIndexerManagerService).isNull();
+    }
+}
diff --git a/testing/mockingservicestests/src/com/android/server/appsearch/MockingFrameworkOptimizeStrategyTest.java b/testing/mockingservicestests/src/com/android/server/appsearch/MockingServiceOptimizeStrategyTest.java
similarity index 65%
rename from testing/mockingservicestests/src/com/android/server/appsearch/MockingFrameworkOptimizeStrategyTest.java
rename to testing/mockingservicestests/src/com/android/server/appsearch/MockingServiceOptimizeStrategyTest.java
index ca4acaf..4eabe13 100644
--- a/testing/mockingservicestests/src/com/android/server/appsearch/MockingFrameworkOptimizeStrategyTest.java
+++ b/testing/mockingservicestests/src/com/android/server/appsearch/MockingServiceOptimizeStrategyTest.java
@@ -29,63 +29,68 @@
 import org.junit.Test;
 
 // This class tests the scenario time_optimize_threshold < min_time_optimize_threshold (which
-// shouldn't be the case in an ideal world) as opposed to FrameworkOptimizeStrategyTest which tests
+// shouldn't be the case in an ideal world) as opposed to ServiceOptimizeStrategyTest which tests
 // the scenario time_optimize_threshold > min_time_optimize_threshold.
-public class MockingFrameworkOptimizeStrategyTest {
+public class MockingServiceOptimizeStrategyTest {
     @Rule
     public final TestableDeviceConfig.TestableDeviceConfigRule
             mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
 
     @Test
     public void testShouldNotOptimize_overOtherThresholds_underMinTimeThreshold() {
-        // Create FrameworkAppSearchConfig with min_time_optimize_threshold <
+        // Create ServiceAppSearchConfig with min_time_optimize_threshold <
         // time_optimize_threshold
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_BYTES_OPTIMIZE_THRESHOLD,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_BYTES_OPTIMIZE_THRESHOLD,
                 Integer.toString(147147),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
                 Integer.toString(900),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_DOC_COUNT_OPTIMIZE_THRESHOLD,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_DOC_COUNT_OPTIMIZE_THRESHOLD,
                 Integer.toString(369369),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
                 Integer.toString(0),
                 false);
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
-        FrameworkOptimizeStrategy mFrameworkOptimizeStrategy =
-                new FrameworkOptimizeStrategy(appSearchConfig);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
+        ServiceOptimizeStrategy mServiceOptimizeStrategy =
+                new ServiceOptimizeStrategy(appSearchConfig);
         // Create optimizeInfo with all values above respective thresholds.
         GetOptimizeInfoResultProto optimizeInfo =
                 GetOptimizeInfoResultProto.newBuilder()
                         .setTimeSinceLastOptimizeMs(
-                                appSearchConfig.getCachedTimeOptimizeThresholdMs()+1)
+                                appSearchConfig.getCachedTimeOptimizeThresholdMs() + 1)
                         .setEstimatedOptimizableBytes(
-                                appSearchConfig.getCachedBytesOptimizeThreshold()+1)
+                                appSearchConfig.getCachedBytesOptimizeThreshold() + 1)
                         .setOptimizableDocs(
-                                appSearchConfig.getCachedDocCountOptimizeThreshold()+1)
+                                appSearchConfig.getCachedDocCountOptimizeThreshold() + 1)
                         .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
                         .build();
 
         // Verify shouldOptimize() returns true when
         // min_time_optimize_threshold(0) < time_optimize_threshold(900)
         // < timeSinceLastOptimize(901)
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
 
         // Set min_time_optimize_threshold to a value greater than time_optimize_threshold
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS,
                 Integer.toString(1000),
                 false);
 
         // Verify shouldOptimize() returns false when
         // min_time_optimize_threshold(1000) > timeSinceLastOptimize(901)
         // > time_optimize_threshold(900)
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
     }
 }
diff --git a/testing/mockingservicestests/src/com/android/server/appsearch/FrameworkAppSearchConfigTest.java b/testing/mockingservicestests/src/com/android/server/appsearch/ServiceAppSearchConfigTest.java
similarity index 81%
rename from testing/mockingservicestests/src/com/android/server/appsearch/FrameworkAppSearchConfigTest.java
rename to testing/mockingservicestests/src/com/android/server/appsearch/ServiceAppSearchConfigTest.java
index f583334..fdd91a5 100644
--- a/testing/mockingservicestests/src/com/android/server/appsearch/FrameworkAppSearchConfigTest.java
+++ b/testing/mockingservicestests/src/com/android/server/appsearch/ServiceAppSearchConfigTest.java
@@ -17,77 +17,82 @@
 package com.android.server.appsearch;
 
 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_API_CALL_STATS_LIMIT;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_BYTES_OPTIMIZE_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_LITE_INDEX_SORT_AT_INDEXING;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_LITE_INDEX_SORT_SIZE;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_RATE_LIMIT_ENABLED;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_SAMPLING_INTERVAL;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_API_CALL_STATS_LIMIT;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_BYTES_OPTIMIZE_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_DENYLIST;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_DOC_COUNT_OPTIMIZE_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_COMPRESSION_LEVEL;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_INDEX_MERGE_SIZE;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_LITE_INDEX_SORT_AT_INDEXING;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_LITE_INDEX_SORT_SIZE;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_MAX_PAGE_BYTES_LIMIT;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_MAX_TOKEN_LENGTH;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_USE_PERSISTENT_HASHMAP;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_ICING_USE_READ_ONLY_SEARCH;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_API_COSTS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_ENABLED;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS;
-import static com.android.server.appsearch.FrameworkAppSearchConfigImpl.KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_API_CALL_STATS_LIMIT;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_BYTES_OPTIMIZE_THRESHOLD;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_DENYLIST;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_DOC_COUNT_OPTIMIZE_THRESHOLD;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_FULLY_PERSIST_JOB_INTERVAL;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_COMPRESSION_LEVEL;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_DOCUMENT_STORE_NAMESPACE_ID_FINGERPRINT;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_INDEX_MERGE_SIZE;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_LITE_INDEX_SORT_AT_INDEXING;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_LITE_INDEX_SORT_SIZE;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_MAX_PAGE_BYTES_LIMIT;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_MAX_TOKEN_LENGTH;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_OPTIMIZE_REBUILD_INDEX_THRESHOLD;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_USE_PERSISTENT_HASHMAP;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_USE_PRE_MAPPING_WITH_FILE_BACKED_VECTOR;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_ICING_USE_READ_ONLY_SEARCH;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_LIMIT_CONFIG_MAX_SUGGESTION_COUNT;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_MIN_TIME_OPTIMIZE_THRESHOLD_MILLIS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_API_COSTS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_ENABLED;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS;
+import static com.android.server.appsearch.FrameworkServiceAppSearchConfig.KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_API_CALL_STATS_LIMIT;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_BYTES_OPTIMIZE_THRESHOLD;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_FULLY_PERSIST_JOB_INTERVAL;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_ICING_CONFIG_USE_READ_ONLY_SEARCH;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_INTEGER_INDEX_BUCKET_SPLIT_THRESHOLD;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_LIMIT_CONFIG_MAX_SUGGESTION_COUNT;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_LITE_INDEX_SORT_AT_INDEXING;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_LITE_INDEX_SORT_SIZE;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_RATE_LIMIT_ENABLED;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_SAMPLING_INTERVAL;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS;
 import static com.android.server.appsearch.external.localstorage.IcingOptionsConfig.DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.provider.DeviceConfig;
+
 import com.android.modules.utils.testing.TestableDeviceConfig;
 import com.android.server.appsearch.external.localstorage.AppSearchConfig;
 import com.android.server.appsearch.external.localstorage.IcingOptionsConfig;
 import com.android.server.appsearch.external.localstorage.stats.CallStats;
+
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 
-public class FrameworkAppSearchConfigTest {
+public class ServiceAppSearchConfigTest {
     @Rule
     public final TestableDeviceConfig.TestableDeviceConfigRule
             mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
 
     @Test
     public void testDefaultValues_allCachedValue() {
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedMinTimeIntervalBetweenSamplesMillis()).isEqualTo(
                 DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS);
@@ -161,7 +166,9 @@
                 DEFAULT_LITE_INDEX_SORT_AT_INDEXING);
         assertThat(appSearchConfig.getLiteIndexSortSize()).isEqualTo(DEFAULT_LITE_INDEX_SORT_SIZE);
         assertThat(appSearchConfig.getUseNewQualifiedIdJoinIndex())
-            .isEqualTo(DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX);
+                .isEqualTo(DEFAULT_USE_NEW_QUALIFIED_ID_JOIN_INDEX);
+        assertThat(appSearchConfig.getCachedFullyPersistJobIntervalMillis())
+                .isEqualTo(DEFAULT_FULLY_PERSIST_JOB_INTERVAL);
     }
 
     @Test
@@ -172,8 +179,8 @@
                 Long.toString(minTimeIntervalBetweenSamplesMillis),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedMinTimeIntervalBetweenSamplesMillis()).isEqualTo(
                 minTimeIntervalBetweenSamplesMillis);
@@ -186,8 +193,8 @@
                 KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
                 Long.toString(minTimeIntervalBetweenSamplesMillis),
                 false);
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         minTimeIntervalBetweenSamplesMillis = -2;
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
@@ -238,8 +245,8 @@
                 Integer.toString(samplingIntervalOptimizeStats),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedSamplingIntervalDefault()).isEqualTo(
                 samplingIntervalDefault);
@@ -294,8 +301,8 @@
                 KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS,
                 Integer.toString(samplingIntervalOptimizeStats),
                 false);
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // Overrides
         samplingIntervalDefault = -4;
@@ -363,8 +370,8 @@
                 Integer.toString(samplingIntervalPutDocumentStats),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedSamplingIntervalForPutDocumentStats()).isEqualTo(
                 samplingIntervalPutDocumentStats);
@@ -387,8 +394,8 @@
                 Integer.toString(samplingIntervalDefault),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedSamplingIntervalForPutDocumentStats()).isEqualTo(
                 samplingIntervalPutDocumentStats);
@@ -410,8 +417,8 @@
                 Integer.toString(samplingIntervalDefault),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // Sampling values changed.
         samplingIntervalPutDocumentStats = -3;
@@ -445,8 +452,8 @@
                 Integer.toString(samplingIntervalBatchCallStats),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // Default sampling interval changed.
         samplingIntervalDefault = -3;
@@ -470,8 +477,8 @@
                 Integer.toString(2002),
                 /*makeDefault=*/ false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
         assertThat(appSearchConfig.getMaxDocumentSizeBytes()).isEqualTo(2001);
         assertThat(appSearchConfig.getMaxDocumentCount()).isEqualTo(2002);
 
@@ -496,8 +503,8 @@
                 Integer.toString(2003),
                 /*makeDefault=*/ false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
         assertThat(appSearchConfig.getMaxSuggestionCount()).isEqualTo(2003);
 
         // Override
@@ -528,8 +535,8 @@
                 Integer.toString(1000),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedBytesOptimizeThreshold()).isEqualTo(147147);
         assertThat(appSearchConfig.getCachedTimeOptimizeThresholdMs()).isEqualTo(258258);
@@ -556,8 +563,8 @@
                 Integer.toString(1000),
                 false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // Override
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
@@ -589,8 +596,8 @@
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_API_CALL_STATS_LIMIT, Long.toString(dumpsysStatsLimit), false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getCachedApiCallStatsLimit()).isEqualTo(dumpsysStatsLimit);
     }
@@ -600,8 +607,8 @@
         long dumpsysStatsLimit = 10;
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_API_CALL_STATS_LIMIT, Long.toString(dumpsysStatsLimit), false);
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         long newDumpsysStatsLimit = 20;
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
@@ -615,8 +622,8 @@
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_DENYLIST, "pkg=foo&db=bar&apis=localSetSchema,localGetSchema", false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
         assertThat(appSearchConfig.getCachedDenylist().checkDeniedPackageDatabase("foo", "bar",
                 CallStats.CALL_TYPE_SET_SCHEMA)).isTrue();
         assertThat(appSearchConfig.getCachedDenylist().checkDeniedPackageDatabase("foo", "bar",
@@ -627,8 +634,8 @@
 
     @Test
     public void testCustomizedValueOverride_denylist() {
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // By default, denylist should be empty
         for (Integer apiType : CallStats.getAllApiCallTypes()) {
@@ -683,8 +690,8 @@
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_ICING_LITE_INDEX_SORT_SIZE, Integer.toString(1003), false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
         assertThat(appSearchConfig.getMaxTokenLength()).isEqualTo(15);
         assertThat(appSearchConfig.getIndexMergeSize()).isEqualTo(1000);
         assertThat(appSearchConfig.getDocumentStoreNamespaceIdFingerprint()).isEqualTo(true);
@@ -726,8 +733,8 @@
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
                 KEY_ICING_LITE_INDEX_SORT_SIZE, Integer.toString(1003), false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // Override
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
@@ -771,8 +778,8 @@
 
     @Test
     public void testCustomizedValueOverride_rateLimitConfig() {
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
         assertThat(appSearchConfig.getCachedRateLimitEnabled()).isEqualTo(
                 DEFAULT_RATE_LIMIT_ENABLED);
         AppSearchRateLimitConfig rateLimitConfig = appSearchConfig.getCachedRateLimitConfig();
@@ -826,34 +833,64 @@
 
     @Test
     public void testCustomizedValue_joins() {
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-            KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX, Boolean.toString(true), false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX,
+                Boolean.toString(true),
+                false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         assertThat(appSearchConfig.getUseNewQualifiedIdJoinIndex()).isEqualTo(true);
     }
 
     @Test
     public void testCustomizedValueOverride_joins() {
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-            KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX, Boolean.toString(true), false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX,
+                Boolean.toString(true),
+                false);
 
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         // Override
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-            KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX, Boolean.toString(false), false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_USE_NEW_QUALIFIED_ID_JOIN_INDEX,
+                Boolean.toString(false),
+                false);
 
         assertThat(appSearchConfig.getUseNewQualifiedIdJoinIndex()).isEqualTo(false);
     }
 
     @Test
+    public void testCustomizedValueOverride_fullyPersistJobInterval() {
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_FULLY_PERSIST_JOB_INTERVAL,
+                Integer.toString(2003),
+                /*makeDefault=*/ false);
+
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
+        assertThat(appSearchConfig.getCachedFullyPersistJobIntervalMillis()).isEqualTo(2003);
+
+        // Override
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
+                KEY_FULLY_PERSIST_JOB_INTERVAL,
+                Integer.toString(1777),
+                /*makeDefault=*/ false);
+
+        assertThat(appSearchConfig.getCachedFullyPersistJobIntervalMillis()).isEqualTo(1777);
+    }
+
+
+    @Test
     public void testNotUsable_afterClose() {
-        FrameworkAppSearchConfig appSearchConfig =
-            FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        ServiceAppSearchConfig appSearchConfig =
+                FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
 
         appSearchConfig.close();
 
@@ -950,5 +987,8 @@
         Assert.assertThrows("Trying to use a closed AppSearchConfig instance.",
                 IllegalStateException.class,
                 () -> appSearchConfig.getUseNewQualifiedIdJoinIndex());
+        Assert.assertThrows("Trying to use a closed AppSearchConfig instance.",
+                IllegalStateException.class,
+                () -> appSearchConfig.getCachedFullyPersistJobIntervalMillis());
     }
 }
diff --git a/testing/mockingservicestests/src/com/android/server/appsearch/stats/MockingPlatformLoggerTest.java b/testing/mockingservicestests/src/com/android/server/appsearch/stats/MockingPlatformLoggerTest.java
index 4a65227..85fca26 100644
--- a/testing/mockingservicestests/src/com/android/server/appsearch/stats/MockingPlatformLoggerTest.java
+++ b/testing/mockingservicestests/src/com/android/server/appsearch/stats/MockingPlatformLoggerTest.java
@@ -26,8 +26,8 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.modules.utils.testing.TestableDeviceConfig;
-import com.android.server.appsearch.FrameworkAppSearchConfig;
-import com.android.server.appsearch.FrameworkAppSearchConfigImpl;
+import com.android.server.appsearch.FrameworkServiceAppSearchConfig;
+import com.android.server.appsearch.ServiceAppSearchConfig;
 import com.android.server.appsearch.external.localstorage.stats.CallStats;
 import com.android.server.appsearch.util.ApiCallRecord;
 
@@ -50,7 +50,7 @@
     private static final int TEST_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS = 100;
     private static final int TEST_DEFAULT_SAMPLING_INTERVAL = 10;
     private static final String TEST_PACKAGE_NAME = "packageName";
-    private FrameworkAppSearchConfig mAppSearchConfig;
+    private ServiceAppSearchConfig mAppSearchConfig;
 
     @Rule
     public final TestableDeviceConfig.TestableDeviceConfigRule
@@ -58,7 +58,7 @@
 
     @Before
     public void setUp() throws Exception {
-        mAppSearchConfig = FrameworkAppSearchConfigImpl.create(DIRECT_EXECUTOR);
+        mAppSearchConfig = FrameworkServiceAppSearchConfig.create(DIRECT_EXECUTOR);
     }
 
     @Test
@@ -67,12 +67,14 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
                 Long.toString(TEST_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT,
                 Integer.toString(TEST_DEFAULT_SAMPLING_INTERVAL),
                 false);
 
@@ -99,20 +101,24 @@
         PlatformLogger logger = new PlatformLogger(
                 ApplicationProvider.getApplicationContext(), mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
                 Long.toString(TEST_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT,
                 Integer.toString(TEST_DEFAULT_SAMPLING_INTERVAL),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
                 Integer.toString(putDocumentSamplingInterval),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
                 Integer.toString(batchCallSamplingInterval),
                 false);
 
@@ -145,8 +151,9 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT,
                 Long.toString(1),
                 false);
 
@@ -163,8 +170,9 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT,
                 Long.toString(-1),
                 false);
 
@@ -186,12 +194,14 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT,
                 Long.toString(samplingInterval),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
                 Long.toString(minTimeIntervalBetweenSamplesMillis),
                 false);
         logger.setLastPushTimeMillisLocked(SystemClock.elapsedRealtime());
@@ -213,12 +223,14 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_SAMPLING_INTERVAL_DEFAULT,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_SAMPLING_INTERVAL_DEFAULT,
                 Long.toString(samplingInterval),
                 false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
                 Long.toString(minTimeIntervalBetweenSamplesMillis),
                 false);
         logger.setLastPushTimeMillisLocked(SystemClock.elapsedRealtime());
@@ -235,8 +247,11 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_API_CALL_STATS_LIMIT, "0", false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_API_CALL_STATS_LIMIT,
+                "0",
+                false);
 
         logger.addStatsToQueueLocked(
                 new ApiCallRecord(new CallStats.Builder()
@@ -258,8 +273,11 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_API_CALL_STATS_LIMIT, "-1", false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_API_CALL_STATS_LIMIT,
+                "-1",
+                false);
 
         logger.addStatsToQueueLocked(
                 new ApiCallRecord(new CallStats.Builder()
@@ -281,8 +299,11 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_API_CALL_STATS_LIMIT, "1", false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_API_CALL_STATS_LIMIT,
+                "1",
+                false);
 
         logger.addStatsToQueueLocked(
                 new ApiCallRecord(new CallStats.Builder()
@@ -321,8 +342,11 @@
                 ApplicationProvider.getApplicationContext(),
                 mAppSearchConfig);
 
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_API_CALL_STATS_LIMIT, "2", false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_API_CALL_STATS_LIMIT,
+                "2",
+                false);
 
         logger.addStatsToQueueLocked(
                 new ApiCallRecord(new CallStats.Builder()
@@ -346,8 +370,11 @@
         assertThat(logger.getLastCalledApis()).hasSize(2);
 
         // Changing the capacity to 1 will drop the earliest stats.
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_APPSEARCH,
-                FrameworkAppSearchConfigImpl.KEY_API_CALL_STATS_LIMIT, "1", false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_APPSEARCH,
+                FrameworkServiceAppSearchConfig.KEY_API_CALL_STATS_LIMIT,
+                "1",
+                false);
         assertThat(logger.getLastCalledApis()).hasSize(1);
         ApiCallRecord apiCallRecord = logger.getLastCalledApis().get(0);
         assertThat(apiCallRecord.toString()).contains("test_package2");
diff --git a/testing/safeparceltests/src/android/app/appsearch/safeparcel/TestSafeParcelableV4.java b/testing/safeparceltests/src/android/app/appsearch/safeparcel/TestSafeParcelableV4.java
new file mode 100644
index 0000000..93096d4
--- /dev/null
+++ b/testing/safeparceltests/src/android/app/appsearch/safeparcel/TestSafeParcelableV4.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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.safeparcel;
+
+import android.os.Parcel;
+
[email protected](creator = "TestSafeParcelableV4Creator")
+public class TestSafeParcelableV4<T> extends AbstractSafeParcelable {
+
+    @SuppressWarnings("rawtypes")
+    public static final Creator<TestSafeParcelableV4> CREATOR = new TestSafeParcelableV4Creator();
+
+    @Field(id = 1)
+    public String publicString;
+
+    @Constructor
+    public TestSafeParcelableV4(@Param(id = 1) String publicString) {
+        this.publicString = publicString;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        TestSafeParcelableV4Creator.writeToParcel(this, out, flags);
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/AppSearchRateLimitConfigTest.java b/testing/servicestests/src/com/android/server/appsearch/AppSearchRateLimitConfigTest.java
index 9ccc298..39b8a67 100644
--- a/testing/servicestests/src/com/android/server/appsearch/AppSearchRateLimitConfigTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/AppSearchRateLimitConfigTest.java
@@ -16,9 +16,9 @@
 
 package com.android.server.appsearch;
 
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
-import static com.android.server.appsearch.FrameworkAppSearchConfig.DEFAULT_RATE_LIMIT_API_COSTS_STRING;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_TOTAL_CAPACITY;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_RATE_LIMIT_TASK_QUEUE_PER_PACKAGE_CAPACITY_PERCENTAGE;
+import static com.android.server.appsearch.ServiceAppSearchConfig.DEFAULT_RATE_LIMIT_API_COSTS_STRING;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/testing/servicestests/src/com/android/server/appsearch/DenylistTest.java b/testing/servicestests/src/com/android/server/appsearch/DenylistTest.java
index 640f410..7c0e2df 100644
--- a/testing/servicestests/src/com/android/server/appsearch/DenylistTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/DenylistTest.java
@@ -48,7 +48,7 @@
                         + "localSearchSuggestion,globalReportUsage,localReportUsage,"
                         + "localRemoveByDocumentId,localRemoveBySearch,localGetStorageInfo,flush,"
                         + "globalRegisterObserverCallback,globalUnregisterObserverCallback,"
-                        + "initialize");
+                        + "initialize,executeAppFunction");
         for (Integer apiType : CallStats.getAllApiCallTypes()) {
             assertThat(denylist.checkDeniedPackageDatabase("foo", "bar", apiType)).isTrue();
             assertThat(denylist.checkDeniedPackageDatabase("bar", "foo", apiType)).isFalse();
diff --git a/testing/servicestests/src/com/android/server/appsearch/FrameworkOptimizeStrategyTest.java b/testing/servicestests/src/com/android/server/appsearch/ServiceOptimizeStrategyTest.java
similarity index 78%
rename from testing/servicestests/src/com/android/server/appsearch/FrameworkOptimizeStrategyTest.java
rename to testing/servicestests/src/com/android/server/appsearch/ServiceOptimizeStrategyTest.java
index 626422f..a70f8aa 100644
--- a/testing/servicestests/src/com/android/server/appsearch/FrameworkOptimizeStrategyTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/ServiceOptimizeStrategyTest.java
@@ -26,13 +26,13 @@
 
 // NOTE: The tests in this class are based on the underlying assumption that
 // time_optimize_threshold > min_time_optimize_threshold. This ensures that setting
-// timeSinceLastOptimize to time_optimize_threshold-1 does not make it lesser than
+// timeSinceLastOptimize to time_optimize_threshold - 1 does not make it lesser than
 // min_time_optimize_threshold (otherwise shouldOptimize() would return false for test cases that
 // check byteThreshold and docCountThreshold).
-public class FrameworkOptimizeStrategyTest {
-    FrameworkAppSearchConfig mAppSearchConfig = new FakeAppSearchConfig();
-    FrameworkOptimizeStrategy mFrameworkOptimizeStrategy =
-            new FrameworkOptimizeStrategy(mAppSearchConfig);
+public class ServiceOptimizeStrategyTest {
+    ServiceAppSearchConfig mAppSearchConfig = new FakeAppSearchConfig();
+    ServiceOptimizeStrategy mServiceOptimizeStrategy =
+            new ServiceOptimizeStrategy(mAppSearchConfig);
 
     @Test
     public void testTimeOptimizeThreshold_isGreaterThan_minTimeOptimizeThreshold() {
@@ -45,14 +45,14 @@
         GetOptimizeInfoResultProto optimizeInfo =
                 GetOptimizeInfoResultProto.newBuilder()
                         .setTimeSinceLastOptimizeMs(
-                                mAppSearchConfig.getCachedTimeOptimizeThresholdMs()-1)
+                                mAppSearchConfig.getCachedTimeOptimizeThresholdMs() - 1)
                         .setEstimatedOptimizableBytes(
-                                mAppSearchConfig.getCachedBytesOptimizeThreshold()-1)
+                                mAppSearchConfig.getCachedBytesOptimizeThreshold() - 1)
                         .setOptimizableDocs(
-                                mAppSearchConfig.getCachedDocCountOptimizeThreshold()-1)
+                                mAppSearchConfig.getCachedDocCountOptimizeThreshold() - 1)
                         .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
                         .build();
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
     }
 
     @Test
@@ -60,14 +60,14 @@
         GetOptimizeInfoResultProto optimizeInfo =
                 GetOptimizeInfoResultProto.newBuilder()
                         .setTimeSinceLastOptimizeMs(
-                                mAppSearchConfig.getCachedTimeOptimizeThresholdMs()-1)
+                                mAppSearchConfig.getCachedTimeOptimizeThresholdMs() - 1)
                         .setEstimatedOptimizableBytes(
                                 mAppSearchConfig.getCachedBytesOptimizeThreshold())
                         .setOptimizableDocs(
-                                mAppSearchConfig.getCachedDocCountOptimizeThreshold()-1)
+                                mAppSearchConfig.getCachedDocCountOptimizeThreshold() - 1)
                         .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
                         .build();
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
     }
 
     @Test
@@ -77,12 +77,12 @@
                         .setTimeSinceLastOptimizeMs(
                                 mAppSearchConfig.getCachedTimeOptimizeThresholdMs())
                         .setEstimatedOptimizableBytes(
-                                mAppSearchConfig.getCachedBytesOptimizeThreshold()-1)
+                                mAppSearchConfig.getCachedBytesOptimizeThreshold() - 1)
                         .setOptimizableDocs(
-                                mAppSearchConfig.getCachedDocCountOptimizeThreshold()-1)
+                                mAppSearchConfig.getCachedDocCountOptimizeThreshold() - 1)
                         .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
                         .build();
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
     }
 
     @Test
@@ -90,14 +90,13 @@
         GetOptimizeInfoResultProto optimizeInfo =
                 GetOptimizeInfoResultProto.newBuilder()
                         .setTimeSinceLastOptimizeMs(
-                                mAppSearchConfig.getCachedTimeOptimizeThresholdMs()-1)
+                                mAppSearchConfig.getCachedTimeOptimizeThresholdMs() - 1)
                         .setEstimatedOptimizableBytes(
-                                mAppSearchConfig.getCachedBytesOptimizeThreshold()-1)
-                        .setOptimizableDocs(
-                                mAppSearchConfig.getCachedDocCountOptimizeThreshold())
+                                mAppSearchConfig.getCachedBytesOptimizeThreshold() - 1)
+                        .setOptimizableDocs(mAppSearchConfig.getCachedDocCountOptimizeThreshold())
                         .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
                         .build();
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isTrue();
     }
 
     @Test
@@ -105,13 +104,12 @@
         GetOptimizeInfoResultProto optimizeInfo =
                 GetOptimizeInfoResultProto.newBuilder()
                         .setTimeSinceLastOptimizeMs(
-                                mAppSearchConfig.getCachedMinTimeOptimizeThresholdMs()-1)
+                                mAppSearchConfig.getCachedMinTimeOptimizeThresholdMs() - 1)
                         .setEstimatedOptimizableBytes(
                                 mAppSearchConfig.getCachedBytesOptimizeThreshold())
-                        .setOptimizableDocs(
-                                mAppSearchConfig.getCachedDocCountOptimizeThreshold())
+                        .setOptimizableDocs(mAppSearchConfig.getCachedDocCountOptimizeThreshold())
                         .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK).build())
                         .build();
-        assertThat(mFrameworkOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
+        assertThat(mServiceOptimizeStrategy.shouldOptimize(optimizeInfo)).isFalse();
     }
 }
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java
index 35ae04a..1e8d2f5 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchImplTest.java
@@ -128,8 +128,8 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
     }
 
@@ -485,9 +485,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -497,10 +497,10 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.remove(
-                "package", "database", "namespace", "id", /*removeStatsBuilder=*/ null);
+                "package", "database", "namespace", "id", /* removeStatsBuilder= */ null);
 
         // Verify there is garbage documents.
         GetOptimizeInfoResultProto optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
@@ -508,7 +508,7 @@
 
         // Increase mutation counter and stop before reach the threshold
         mAppSearchImpl.checkForOptimize(
-                AppSearchImpl.CHECK_OPTIMIZE_INTERVAL - 1, /*builder=*/ null);
+                AppSearchImpl.CHECK_OPTIMIZE_INTERVAL - 1, /* builder= */ null);
 
         // Verify the optimize() isn't triggered.
         optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
@@ -516,7 +516,7 @@
 
         // Increase the counter and reach the threshold, optimize() should be triggered.
         OptimizeStats.Builder builder = new OptimizeStats.Builder();
-        mAppSearchImpl.checkForOptimize(/*mutateBatchSize=*/ 1, builder);
+        mAppSearchImpl.checkForOptimize(/* mutateBatchSize= */ 1, builder);
 
         // Verify optimize() is triggered.
         optimizeInfo = mAppSearchImpl.getOptimizeInfoResultLocked();
@@ -541,9 +541,9 @@
                         mContext.getPackageName(),
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -554,17 +554,17 @@
                 mContext.getPackageName(),
                 "database1",
                 validDoc,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query it via global query. We use the same code again later so this is to make sure we
         // have our global query configured right.
         SearchResultPage results =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         new SearchSpec.Builder().addFilterSchemas("Type1").build(),
                         mSelfCallerAccess,
-                        /*logger=*/ null);
+                        /* logger= */ null);
         assertThat(results.getResults()).hasSize(1);
         assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
 
@@ -598,7 +598,7 @@
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
                         initStatsBuilder,
-                        /*visibilityChecker=*/ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Check recovery state
@@ -621,17 +621,17 @@
         assertThat(
                         mAppSearchImpl
                                 .getSchema(
-                                        /*packageName=*/ mContext.getPackageName(),
-                                        /*databaseName=*/ "database1",
-                                        /*callerAccess=*/ mSelfCallerAccess)
+                                        /* packageName= */ mContext.getPackageName(),
+                                        /* databaseName= */ "database1",
+                                        /* callerAccess= */ mSelfCallerAccess)
                                 .getSchemas())
                 .isEmpty();
         results =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         new SearchSpec.Builder().addFilterSchemas("Type1").build(),
                         mSelfCallerAccess,
-                        /*logger=*/ null);
+                        /* logger= */ null);
         assertThat(results.getResults()).isEmpty();
 
         // Make sure the index can now be used successfully
@@ -640,9 +640,9 @@
                         mContext.getPackageName(),
                         "database1",
                         Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -651,16 +651,16 @@
                 mContext.getPackageName(),
                 "database1",
                 validDoc,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query it via global query.
         results =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         new SearchSpec.Builder().addFilterSchemas("Type1").build(),
                         mSelfCallerAccess,
-                        /*logger=*/ null);
+                        /* logger= */ null);
         assertThat(results.getResults()).hasSize(1);
         assertThat(results.getResults().get(0).getGenericDocument()).isEqualTo(validDoc);
     }
@@ -670,7 +670,8 @@
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package", "EmptyDatabase", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query(
+                        "package", "EmptyDatabase", "", searchSpec, /* logger= */ null);
         assertThat(searchResultPage.getResults()).isEmpty();
     }
 
@@ -688,9 +689,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -702,9 +703,9 @@
                         "package2",
                         "database2",
                         schema2,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -715,14 +716,14 @@
                 "package1",
                 "database1",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // No query filters specified, package2 shouldn't be able to query for package1's documents.
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package2", "database2", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
         assertThat(searchResultPage.getResults()).isEmpty();
 
         // Insert package2 document
@@ -731,13 +732,12 @@
                 "package2",
                 "database2",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // No query filters specified. package2 should only get its own documents back.
         searchResultPage =
-                mAppSearchImpl.query("package2", "database2", "", searchSpec, /*logger=
-         */ null);
+                mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
     }
@@ -756,9 +756,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -770,9 +770,9 @@
                         "package2",
                         "database2",
                         schema2,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -783,8 +783,8 @@
                 "package1",
                 "database1",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // "package1" filter specified, but package2 shouldn't be able to query for package1's
         // documents.
@@ -794,7 +794,7 @@
                         .addFilterPackageNames("package1")
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package2", "database2", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
         assertThat(searchResultPage.getResults()).isEmpty();
 
         // Insert package2 document
@@ -803,8 +803,8 @@
                 "package2",
                 "database2",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // "package2" filter specified, package2 should only get its own documents back.
         searchSpec =
@@ -813,8 +813,7 @@
                         .addFilterPackageNames("package2")
                         .build();
         searchResultPage =
-                mAppSearchImpl.query("package2", "database2", "", searchSpec, /*logger=
-         */ null);
+                mAppSearchImpl.query("package2", "database2", "", searchSpec, /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
     }
@@ -825,10 +824,10 @@
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
         SearchResultPage searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        new CallerAccess(/*callingPackageName=*/ ""),
-                        /*logger=*/ null);
+                        new CallerAccess(/* callingPackageName= */ ""),
+                        /* logger= */ null);
         assertThat(searchResultPage.getResults()).isEmpty();
     }
 
@@ -844,7 +843,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -856,9 +855,9 @@
                         "package1",
                         "database1",
                         personSchema,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -877,9 +876,9 @@
                         "package2",
                         "database2",
                         callSchema,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -891,9 +890,9 @@
                         "package3",
                         "database3",
                         textSchema,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -904,8 +903,8 @@
                 "package1",
                 "database1",
                 person,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Insert package2 document
         GenericDocument call =
@@ -916,8 +915,8 @@
                 "package2",
                 "database2",
                 call,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Insert package3 document
         GenericDocument text =
@@ -928,8 +927,8 @@
                 "package3",
                 "database3",
                 text,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Filter on parent spec only
         SearchSpec nested =
@@ -947,7 +946,7 @@
                         .build();
         SearchResultPage searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        "", searchSpec, new CallerAccess("package1"), /*logger=*/ null);
+                        "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
         SearchResult result = searchResultPage.getResults().get(0);
@@ -963,7 +962,7 @@
                         .build();
         searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        "", searchSpec, new CallerAccess("package1"), /*logger=*/ null);
+                        "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(3);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call);
@@ -984,7 +983,7 @@
                         .build();
         searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        "", searchSpec, new CallerAccess("package1"), /*logger=*/ null);
+                        "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(3);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
         assertThat(searchResultPage.getResults().get(1).getGenericDocument()).isEqualTo(call);
@@ -1002,7 +1001,7 @@
                         .build();
         searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        "", searchSpec, new CallerAccess("package1"), /*logger=*/ null);
+                        "", searchSpec, new CallerAccess("package1"), /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
         result = searchResultPage.getResults().get(0);
@@ -1025,7 +1024,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -1046,9 +1045,9 @@
                         "package1",
                         "database1",
                         personAndCallSchema,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1061,9 +1060,9 @@
                         "package2",
                         "database2",
                         callSchema,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1074,8 +1073,8 @@
                 "package1",
                 "database1",
                 person,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         GenericDocument call1 =
                 new GenericDocument.Builder<>("namespace", "id1", "callSchema")
@@ -1091,16 +1090,16 @@
                 "package1",
                 "database1",
                 call1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Insert package2 action document
         mAppSearchImpl.putDocument(
                 "package2",
                 "database2",
                 call2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Invalid parent spec filter
         SearchSpec searchSpec =
@@ -1111,7 +1110,7 @@
                         .setOrder(SearchSpec.ORDER_ASCENDING)
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
         // Only package1 documents should be returned
         assertThat(searchResultPage.getResults()).hasSize(2);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
@@ -1134,7 +1133,7 @@
                         .setJoinSpec(join)
                         .build();
         searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Only package1 documents should be returned, for both the outer and nested searches
         assertThat(searchResultPage.getResults()).hasSize(2);
@@ -1161,7 +1160,7 @@
                         .setJoinSpec(join)
                         .build();
         searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Package1 documents should be returned, but no packages should be joined
         assertThat(searchResultPage.getResults()).hasSize(2);
@@ -1186,7 +1185,7 @@
                         .setJoinSpec(join)
                         .build();
         searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Only package1 documents should be returned, for both the outer and nested searches
         assertThat(searchResultPage.getResults()).hasSize(2);
@@ -1208,7 +1207,7 @@
                         .setJoinSpec(join)
                         .build();
         searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
         // Should work as expected
         assertThat(searchResultPage.getResults()).hasSize(2);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(person);
@@ -1238,9 +1237,9 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
 
         // Insert three documents.
@@ -1257,18 +1256,30 @@
                         .setPropertyString("body", "termOne termTwo termThree")
                         .build();
         mAppSearchImpl.putDocument(
-                "package", "database", doc1, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc1,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
-                "package", "database", doc2, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc2,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
-                "package", "database", doc3, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc3,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         List<SearchSuggestionResult> suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).hasSize(3);
         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("termone");
         assertThat(suggestions.get(1).getSuggestedResult()).isEqualTo("termtwo");
@@ -1279,8 +1290,8 @@
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 2).build());
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 2).build());
         assertThat(suggestions).hasSize(2);
         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("termone");
         assertThat(suggestions.get(1).getSuggestedResult()).isEqualTo("termtwo");
@@ -1306,9 +1317,9 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
 
         // Insert a document.
@@ -1317,28 +1328,32 @@
                         .setPropertyString("body", "termOne")
                         .build();
         mAppSearchImpl.putDocument(
-                "package", "database", doc1, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc1,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         List<SearchSuggestionResult> suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).hasSize(1);
         assertThat(suggestions.get(0).getSuggestedResult()).isEqualTo("termone");
 
         // Remove the document.
         mAppSearchImpl.remove(
-                "package", "database", "namespace", "id1", /*removeStatsBuilder=*/ null);
+                "package", "database", "namespace", "id1", /* removeStatsBuilder= */ null);
 
         // Now we cannot find any suggestion
         suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).isEmpty();
     }
 
@@ -1362,9 +1377,9 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
 
         // Insert a document.
@@ -1373,7 +1388,11 @@
                         .setPropertyString("body", "tart two three")
                         .build();
         mAppSearchImpl.putDocument(
-                "package", "database", doc1, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc1,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         SearchSuggestionResult tartResult =
                 new SearchSuggestionResult.Builder().setSuggestedResult("tart").build();
         SearchSuggestionResult twoResult =
@@ -1386,8 +1405,8 @@
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).containsExactly(tartResult, twoResult, threeResult);
 
         // replace the document with two terms.
@@ -1399,16 +1418,16 @@
                 "package",
                 "database",
                 replaceDocument,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we cannot find any suggestion
         suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).containsExactly(twistResult, threeResult);
     }
 
@@ -1432,9 +1451,9 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
 
         // Insert three documents.
@@ -1452,18 +1471,30 @@
                         .build();
 
         mAppSearchImpl.putDocument(
-                "package", "database", doc1, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc1,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
-                "package", "database", doc2, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc2,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
-                "package", "database", doc3, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc3,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         List<SearchSuggestionResult> suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10)
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
                                 .addFilterNamespaces("namespace1")
                                 .build());
         assertThat(suggestions).hasSize(1);
@@ -1473,8 +1504,8 @@
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10)
+                        /* suggestionQueryExpression= */ "t",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
                                 .addFilterNamespaces("namespace1", "namespace2")
                                 .build());
         assertThat(suggestions).hasSize(2);
@@ -1503,51 +1534,55 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
         GenericDocument doc =
                 new GenericDocument.Builder<>("namespace1", "id1", "type")
                         .setPropertyString("body", "term1")
                         .build();
         mAppSearchImpl.putDocument(
-                "package", "database", doc, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         List<SearchSuggestionResult> suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t:",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t:",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).isEmpty();
         suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t-",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t-",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).isEmpty();
         suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "t  ",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "t  ",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).isEmpty();
         suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "{t}",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "{t}",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).isEmpty();
         suggestions =
                 mAppSearchImpl.searchSuggestion(
                         "package",
                         "database",
-                        /*suggestionQueryExpression=*/ "(t)",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10).build());
+                        /* suggestionQueryExpression= */ "(t)",
+                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10).build());
         assertThat(suggestions).isEmpty();
     }
 
@@ -1571,16 +1606,20 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
         GenericDocument doc =
                 new GenericDocument.Builder<>("namespace1", "id1", "type")
                         .setPropertyString("body", "term1")
                         .build();
         mAppSearchImpl.putDocument(
-                "package", "database", doc, /*sendChangeNotifications=*/ false, /*logger=*/ null);
+                "package",
+                "database",
+                doc,
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         AppSearchException e =
                 assertThrows(
@@ -1589,8 +1628,8 @@
                                 mAppSearchImpl.searchSuggestion(
                                         "package",
                                         "database",
-                                        /*suggestionQueryExpression=*/ "",
-                                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10)
+                                        /* suggestionQueryExpression= */ "",
+                                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
                                                 .addFilterNamespaces("namespace1")
                                                 .build()));
         assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
@@ -1607,9 +1646,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1622,14 +1661,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -1638,7 +1677,7 @@
                         .setResultCountPerPage(1)
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1647,7 +1686,7 @@
 
         long nextPageToken = searchResultPage.getNextPageToken();
         searchResultPage =
-                mAppSearchImpl.getNextPage("package1", nextPageToken, /*statsBuilder=*/ null);
+                mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
     }
@@ -1662,9 +1701,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1677,14 +1716,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -1693,7 +1732,7 @@
                         .setResultCountPerPage(1)
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1708,7 +1747,7 @@
                         AppSearchException.class,
                         () ->
                                 mAppSearchImpl.getNextPage(
-                                        "package2", nextPageToken, /*statsBuilder=*/ null));
+                                        "package2", nextPageToken, /* statsBuilder= */ null));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken);
@@ -1716,7 +1755,7 @@
 
         // Can continue getting next page for package1
         searchResultPage =
-                mAppSearchImpl.getNextPage("package1", nextPageToken, /*statsBuilder=*/ null);
+                mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
     }
@@ -1731,9 +1770,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1746,14 +1785,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -1763,10 +1802,10 @@
                         .build();
         SearchResultPage searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        new CallerAccess(/*callingPackageName=*/ "package1"),
-                        /*logger=*/ null);
+                        new CallerAccess(/* callingPackageName= */ "package1"),
+                        /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1775,7 +1814,7 @@
 
         long nextPageToken = searchResultPage.getNextPageToken();
         searchResultPage =
-                mAppSearchImpl.getNextPage("package1", nextPageToken, /*statsBuilder=*/ null);
+                mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
     }
@@ -1790,9 +1829,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1805,14 +1844,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -1822,10 +1861,10 @@
                         .build();
         SearchResultPage searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        new CallerAccess(/*callingPackageName=*/ "package1"),
-                        /*logger=*/ null);
+                        new CallerAccess(/* callingPackageName= */ "package1"),
+                        /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1840,7 +1879,7 @@
                         AppSearchException.class,
                         () ->
                                 mAppSearchImpl.getNextPage(
-                                        "package2", nextPageToken, /*statsBuilder=*/ null));
+                                        "package2", nextPageToken, /* statsBuilder= */ null));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Package \"package2\" cannot use nextPageToken: " + nextPageToken);
@@ -1848,7 +1887,7 @@
 
         // Can continue getting next page for package1
         searchResultPage =
-                mAppSearchImpl.getNextPage("package1", nextPageToken, /*statsBuilder=*/ null);
+                mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
     }
@@ -1863,9 +1902,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1878,14 +1917,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -1894,7 +1933,7 @@
                         .setResultCountPerPage(1)
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -1912,7 +1951,7 @@
                         AppSearchException.class,
                         () ->
                                 mAppSearchImpl.getNextPage(
-                                        "package1", nextPageToken, /*statsBuilder=*/ null));
+                                        "package1", nextPageToken, /* statsBuilder= */ null));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Package \"package1\" cannot use nextPageToken: " + nextPageToken);
@@ -1929,9 +1968,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1942,8 +1981,8 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for 2 results per page, so all the results can fit in one page.
         SearchSpec searchSpec =
@@ -1953,7 +1992,7 @@
                                 2) // make sure all the results can be returned in one page.
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // We only have one document indexed
         assertThat(searchResultPage.getResults()).hasSize(1);
@@ -1976,9 +2015,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1991,14 +2030,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -2007,7 +2046,7 @@
                         .setResultCountPerPage(1)
                         .build();
         SearchResultPage searchResultPage =
-                mAppSearchImpl.query("package1", "database1", "", searchSpec, /*logger=*/ null);
+                mAppSearchImpl.query("package1", "database1", "", searchSpec, /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -2028,7 +2067,7 @@
 
         // Can continue getting next page for package1
         searchResultPage =
-                mAppSearchImpl.getNextPage("package1", nextPageToken, /*statsBuilder=*/ null);
+                mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
     }
@@ -2043,9 +2082,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2058,14 +2097,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -2075,10 +2114,10 @@
                         .build();
         SearchResultPage searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        new CallerAccess(/*callingPackageName=*/ "package1"),
-                        /*logger=*/ null);
+                        new CallerAccess(/* callingPackageName= */ "package1"),
+                        /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -2096,7 +2135,7 @@
                         AppSearchException.class,
                         () ->
                                 mAppSearchImpl.getNextPage(
-                                        "package1", nextPageToken, /*statsBuilder=*/ null));
+                                        "package1", nextPageToken, /* statsBuilder= */ null));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Package \"package1\" cannot use nextPageToken: " + nextPageToken);
@@ -2113,9 +2152,9 @@
                         "package1",
                         "database1",
                         schema1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2128,14 +2167,14 @@
                 "package1",
                 "database1",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database1",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Query for only 1 result per page
         SearchSpec searchSpec =
@@ -2145,10 +2184,10 @@
                         .build();
         SearchResultPage searchResultPage =
                 mAppSearchImpl.globalQuery(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        new CallerAccess(/*callingPackageName=*/ "package1"),
-                        /*logger=*/ null);
+                        new CallerAccess(/* callingPackageName= */ "package1"),
+                        /* logger= */ null);
 
         // Document2 will come first because it was inserted last and default return order is
         // most recent.
@@ -2169,7 +2208,7 @@
 
         // Can continue getting next page for package1
         searchResultPage =
-                mAppSearchImpl.getNextPage("package1", nextPageToken, /*statsBuilder=*/ null);
+                mAppSearchImpl.getNextPage("package1", nextPageToken, /* statsBuilder= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document1);
     }
@@ -2182,7 +2221,7 @@
                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
                         .build();
         mAppSearchImpl.removeByQuery(
-                "package", "EmptyDatabase", "", searchSpec, /*statsBuilder=*/ null);
+                "package", "EmptyDatabase", "", searchSpec, /* statsBuilder= */ null);
 
         searchSpec =
                 new SearchSpec.Builder()
@@ -2190,11 +2229,11 @@
                         .setTermMatch(TermMatchType.Code.PREFIX_VALUE)
                         .build();
         mAppSearchImpl.removeByQuery(
-                "package", "EmptyDatabase", "", searchSpec, /*statsBuilder=*/ null);
+                "package", "EmptyDatabase", "", searchSpec, /* statsBuilder= */ null);
 
         searchSpec = new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
         mAppSearchImpl.removeByQuery(
-                "package", "EmptyDatabase", "", searchSpec, /*statsBuilder=*/ null);
+                "package", "EmptyDatabase", "", searchSpec, /* statsBuilder= */ null);
     }
 
     @Test
@@ -2210,9 +2249,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2222,6 +2261,7 @@
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .build();
 
@@ -2256,9 +2296,9 @@
                         "package",
                         "database1",
                         oldSchemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2272,9 +2312,9 @@
                         "package",
                         "database1",
                         newSchemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         SetSchemaResponse setSchemaResponse = internalSetSchemaResponse.getSetSchemaResponse();
@@ -2298,9 +2338,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2310,10 +2350,12 @@
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Document")
+                                        .setDescription("")
                                         .setVersion(0))
                         .build();
 
@@ -2331,9 +2373,9 @@
                         "package",
                         "database1",
                         finalSchemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         // We are fail to set this call since forceOverride is false.
         assertThat(internalSetSchemaResponse.isSuccess()).isFalse();
@@ -2347,9 +2389,9 @@
                         "package",
                         "database1",
                         finalSchemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2359,6 +2401,7 @@
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .build();
 
@@ -2386,9 +2429,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -2396,9 +2439,9 @@
                         "package",
                         "database2",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2408,18 +2451,22 @@
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Document")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database2/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database2/Document")
+                                        .setDescription("")
                                         .setVersion(0))
                         .build();
 
@@ -2437,9 +2484,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2450,14 +2497,17 @@
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database1/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database2/Email")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("package$database2/Document")
+                                        .setDescription("")
                                         .setVersion(0))
                         .build();
 
@@ -2483,9 +2533,9 @@
                         "package",
                         "database",
                         schema,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2496,8 +2546,8 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Verify the document is indexed.
         SearchSpec searchSpec =
@@ -2506,9 +2556,9 @@
                 mAppSearchImpl.query(
                         "package",
                         "database",
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*logger=*/ null);
+                        /* logger= */ null);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(document);
 
@@ -2520,9 +2570,9 @@
                 mAppSearchImpl.query(
                         "package2",
                         "database2",
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*logger=*/ null);
+                        /* logger= */ null);
         assertThat(searchResultPage.getResults()).isEmpty();
 
         // Verify the schema is cleared.
@@ -2558,9 +2608,9 @@
                         "packageA",
                         "database",
                         schema,
-                        /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -2568,9 +2618,9 @@
                         "packageB",
                         "database",
                         schema,
-                        /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2580,10 +2630,12 @@
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("packageA$database/schema")
+                                        .setDescription("")
                                         .setVersion(0))
                         .addTypes(
                                 SchemaTypeConfigProto.newBuilder()
                                         .setSchemaType("packageB$database/schema")
+                                        .setDescription("")
                                         .setVersion(0))
                         .build();
         List<SchemaTypeConfigProto> expectedTypes = new ArrayList<>();
@@ -2637,9 +2689,9 @@
                         "package1",
                         "database1",
                         Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(mAppSearchImpl.getPackageToDatabases())
@@ -2652,9 +2704,9 @@
                         "package1",
                         "database2",
                         Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(mAppSearchImpl.getPackageToDatabases())
@@ -2667,9 +2719,9 @@
                         "package2",
                         "database1",
                         Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(mAppSearchImpl.getPackageToDatabases())
@@ -2690,9 +2742,9 @@
                         "package1",
                         "database1",
                         schemas1,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -2700,9 +2752,9 @@
                         "package1",
                         "database2",
                         schemas2,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -2710,9 +2762,9 @@
                         "package2",
                         "database1",
                         schemas3,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(mAppSearchImpl.getAllPrefixedSchemaTypes())
@@ -2736,9 +2788,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2751,14 +2803,14 @@
                 "package",
                 "database",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Report some usages. id1 has 2 app and 1 system usage, id2 has 1 app and 2 system usage.
         mAppSearchImpl.reportUsage(
@@ -2766,44 +2818,44 @@
                 "database",
                 "namespace",
                 "id1",
-                /*usageTimestampMillis=*/ 10,
-                /*systemUsage=*/ false);
+                /* usageTimestampMillis= */ 10,
+                /* systemUsage= */ false);
         mAppSearchImpl.reportUsage(
                 "package",
                 "database",
                 "namespace",
                 "id1",
-                /*usageTimestampMillis=*/ 20,
-                /*systemUsage=*/ false);
+                /* usageTimestampMillis= */ 20,
+                /* systemUsage= */ false);
         mAppSearchImpl.reportUsage(
                 "package",
                 "database",
                 "namespace",
                 "id1",
-                /*usageTimestampMillis=*/ 1000,
-                /*systemUsage=*/ true);
+                /* usageTimestampMillis= */ 1000,
+                /* systemUsage= */ true);
 
         mAppSearchImpl.reportUsage(
                 "package",
                 "database",
                 "namespace",
                 "id2",
-                /*usageTimestampMillis=*/ 100,
-                /*systemUsage=*/ false);
+                /* usageTimestampMillis= */ 100,
+                /* systemUsage= */ false);
         mAppSearchImpl.reportUsage(
                 "package",
                 "database",
                 "namespace",
                 "id2",
-                /*usageTimestampMillis=*/ 200,
-                /*systemUsage=*/ true);
+                /* usageTimestampMillis= */ 200,
+                /* systemUsage= */ true);
         mAppSearchImpl.reportUsage(
                 "package",
                 "database",
                 "namespace",
                 "id2",
-                /*usageTimestampMillis=*/ 150,
-                /*systemUsage=*/ true);
+                /* usageTimestampMillis= */ 150,
+                /* systemUsage= */ true);
 
         // Sort by app usage count: id1 should win
         List<SearchResult> page =
@@ -2816,7 +2868,7 @@
                                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                                         .setRankingStrategy(SearchSpec.RANKING_STRATEGY_USAGE_COUNT)
                                         .build(),
-                                /*logger=*/ null)
+                                /* logger= */ null)
                         .getResults();
         assertThat(page).hasSize(2);
         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
@@ -2835,7 +2887,7 @@
                                                 SearchSpec
                                                         .RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP)
                                         .build(),
-                                /*logger=*/ null)
+                                /* logger= */ null)
                         .getResults();
         assertThat(page).hasSize(2);
         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
@@ -2853,7 +2905,7 @@
                                         .setRankingStrategy(
                                                 SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT)
                                         .build(),
-                                /*logger=*/ null)
+                                /* logger= */ null)
                         .getResults();
         assertThat(page).hasSize(2);
         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id2");
@@ -2872,7 +2924,7 @@
                                                 SearchSpec
                                                         .RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP)
                                         .build(),
-                                /*logger=*/ null)
+                                /* logger= */ null)
                         .getResults();
         assertThat(page).hasSize(2);
         assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id1");
@@ -2898,9 +2950,9 @@
                         "package1",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2922,9 +2974,9 @@
                         "package1",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2935,8 +2987,8 @@
                 "package1",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Insert schema for "package2"
         internalSetSchemaResponse =
@@ -2944,9 +2996,9 @@
                         "package2",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -2956,15 +3008,15 @@
                 "package2",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         document = new GenericDocument.Builder<>("namespace", "id2", "type").build();
         mAppSearchImpl.putDocument(
                 "package2",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForPackage("package1");
         long size1 = storageInfo.getSizeBytes();
@@ -3004,9 +3056,9 @@
                         "package1",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3028,9 +3080,9 @@
                         "package1",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3051,9 +3103,9 @@
                         "package1",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -3061,9 +3113,9 @@
                         "package1",
                         "database2",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3074,8 +3126,8 @@
                 "package1",
                 "database1",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Add two documents for "package1", "database2"
         document = new GenericDocument.Builder<>("namespace1", "id1", "type").build();
@@ -3083,15 +3135,15 @@
                 "package1",
                 "database2",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         document = new GenericDocument.Builder<>("namespace1", "id2", "type").build();
         mAppSearchImpl.putDocument(
                 "package1",
                 "database2",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         StorageInfo storageInfo = mAppSearchImpl.getStorageInfoForDatabase("package1", "database1");
         long size1 = storageInfo.getSizeBytes();
@@ -3120,9 +3172,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3136,18 +3188,18 @@
                                 "package",
                                 "database",
                                 schemas,
-                                /*visibilityConfigs=*/ Collections.emptyList(),
-                                /*forceOverride=*/ false,
-                                /*version=*/ 0,
+                                /* visibilityConfigs= */ Collections.emptyList(),
+                                /* forceOverride= */ false,
+                                /* version= */ 0,
                                 /* setSchemaStatsBuilder= */ null));
 
         assertThrows(
                 IllegalStateException.class,
                 () ->
                         mAppSearchImpl.getSchema(
-                                /*packageName=*/ "package",
-                                /*databaseName=*/ "database",
-                                /*callerAccess=*/ mSelfCallerAccess));
+                                /* packageName= */ "package",
+                                /* databaseName= */ "database",
+                                /* callerAccess= */ mSelfCallerAccess));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3156,8 +3208,8 @@
                                 "package",
                                 "database",
                                 new GenericDocument.Builder<>("namespace", "id", "type").build(),
-                                /*sendChangeNotifications=*/ false,
-                                /*logger=*/ null));
+                                /* sendChangeNotifications= */ false,
+                                /* logger= */ null));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3173,7 +3225,7 @@
                                 "database",
                                 "query",
                                 new SearchSpec.Builder().build(),
-                                /*logger=*/ null));
+                                /* logger= */ null));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3182,17 +3234,17 @@
                                 "query",
                                 new SearchSpec.Builder().build(),
                                 mSelfCallerAccess,
-                                /*logger=*/ null));
+                                /* logger= */ null));
 
         assertThrows(
                 IllegalStateException.class,
                 () ->
                         mAppSearchImpl.getNextPage(
-                                "package", /*nextPageToken=*/ 1L, /*statsBuilder=*/ null));
+                                "package", /* nextPageToken= */ 1L, /* statsBuilder= */ null));
 
         assertThrows(
                 IllegalStateException.class,
-                () -> mAppSearchImpl.invalidateNextPageToken("package", /*nextPageToken=*/ 1L));
+                () -> mAppSearchImpl.invalidateNextPageToken("package", /* nextPageToken= */ 1L));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3202,8 +3254,8 @@
                                 "database",
                                 "namespace",
                                 "id",
-                                /*usageTimestampMillis=*/ 1000L,
-                                /*systemUsage=*/ false));
+                                /* usageTimestampMillis= */ 1000L,
+                                /* systemUsage= */ false));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3213,7 +3265,7 @@
                                 "database",
                                 "namespace",
                                 "id",
-                                /*removeStatsBuilder=*/ null));
+                                /* removeStatsBuilder= */ null));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3223,7 +3275,7 @@
                                 "database",
                                 "query",
                                 new SearchSpec.Builder().build(),
-                                /*removeStatsBuilder=*/ null));
+                                /* removeStatsBuilder= */ null));
 
         assertThrows(
                 IllegalStateException.class,
@@ -3247,9 +3299,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3260,8 +3312,8 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         GenericDocument getResult =
@@ -3275,8 +3327,8 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         getResult =
                 appSearchImpl2.getDocument(
@@ -3294,9 +3346,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3307,16 +3359,16 @@
                 "package",
                 "database",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         GenericDocument document2 =
                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         GenericDocument getResult =
@@ -3329,7 +3381,7 @@
         assertThat(getResult).isEqualTo(document2);
 
         // Delete the first document
-        mAppSearchImpl.remove("package", "database", "namespace1", "id1", /*statsBuilder=*/ null);
+        mAppSearchImpl.remove("package", "database", "namespace1", "id1", /* statsBuilder= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
         assertThrows(
                 AppSearchException.class,
@@ -3351,8 +3403,8 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         assertThrows(
                 AppSearchException.class,
@@ -3379,9 +3431,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3392,16 +3444,16 @@
                 "package",
                 "database",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         GenericDocument document2 =
                 new GenericDocument.Builder<>("namespace2", "id2", "type").build();
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         GenericDocument getResult =
@@ -3422,7 +3474,7 @@
                         .addFilterNamespaces("namespace1")
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .build(),
-                /*statsBuilder=*/ null);
+                /* statsBuilder= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
         assertThrows(
                 AppSearchException.class,
@@ -3444,8 +3496,8 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         assertThrows(
                 AppSearchException.class,
@@ -3472,9 +3524,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3485,16 +3537,16 @@
                 "package",
                 "database",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         GenericDocument document2 =
                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         StorageInfoProto storageInfo = mAppSearchImpl.getRawStorageInfoProto();
 
@@ -3515,9 +3567,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3528,16 +3580,16 @@
                 "package",
                 "database",
                 document1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         GenericDocument document2 =
                 new GenericDocument.Builder<>("namespace1", "id2", "type").build();
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         DebugInfoProto debugInfo =
                 mAppSearchImpl.getRawDebugInfoProto(DebugInfoVerbosity.Code.DETAILED);
@@ -3576,8 +3628,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -3588,9 +3640,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3607,8 +3659,8 @@
                                         "package",
                                         "database",
                                         document,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3624,8 +3676,8 @@
                 "package",
                 "database",
                 document2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we should get a failure
         GenericDocument document3 =
@@ -3638,8 +3690,8 @@
                                         "package",
                                         "database",
                                         document3,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3672,8 +3724,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -3684,9 +3736,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3695,8 +3747,8 @@
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we should get a failure
         GenericDocument document2 =
@@ -3709,8 +3761,8 @@
                                         "package",
                                         "database",
                                         document2,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3739,8 +3791,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Make sure the limit is maintained
@@ -3752,8 +3804,8 @@
                                         "package",
                                         "database",
                                         document2,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3785,8 +3837,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -3797,9 +3849,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3808,20 +3860,20 @@
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id3", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we should get a failure
         GenericDocument document4 =
@@ -3834,8 +3886,8 @@
                                         "package",
                                         "database",
                                         document4,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3850,7 +3902,7 @@
                                 "database",
                                 "namespace",
                                 "id4",
-                                /*removeStatsBuilder=*/ null));
+                                /* removeStatsBuilder= */ null));
 
         // Should still fail
         e =
@@ -3861,8 +3913,8 @@
                                         "package",
                                         "database",
                                         document4,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3870,15 +3922,15 @@
 
         // Remove a document that does exist
         mAppSearchImpl.remove(
-                "package", "database", "namespace", "id2", /*removeStatsBuilder=*/ null);
+                "package", "database", "namespace", "id2", /* removeStatsBuilder= */ null);
 
         // Now doc4 should work
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document4,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // The next one should fail again
         e =
@@ -3890,8 +3942,8 @@
                                         "database",
                                         new GenericDocument.Builder<>("namespace", "id5", "type")
                                                 .build(),
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -3924,8 +3976,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -3936,9 +3988,9 @@
                         "package1",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -3946,9 +3998,9 @@
                         "package1",
                         "database2",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -3956,9 +4008,9 @@
                         "package2",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         internalSetSchemaResponse =
@@ -3966,9 +4018,9 @@
                         "package2",
                         "database2",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -3977,14 +4029,14 @@
                 "package1",
                 "database1",
                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package1",
                 "database2",
                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Indexing a third doc into package1 should fail (here we use database3)
         AppSearchException e =
@@ -3996,8 +4048,8 @@
                                         "database3",
                                         new GenericDocument.Builder<>("namespace", "id3", "type")
                                                 .build(),
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4008,8 +4060,8 @@
                 "package2",
                 "database1",
                 new GenericDocument.Builder<>("namespace", "id1", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Reinitialize to make sure packages are parsed correctly on init
         mAppSearchImpl.close();
@@ -4034,8 +4086,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // package1 should still be out of space
@@ -4048,8 +4100,8 @@
                                         "database4",
                                         new GenericDocument.Builder<>("namespace", "id4", "type")
                                                 .build(),
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4060,8 +4112,8 @@
                 "package2",
                 "database2",
                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // now package2 really is out of space
         e =
@@ -4073,8 +4125,8 @@
                                         "database3",
                                         new GenericDocument.Builder<>("namespace", "id3", "type")
                                                 .build(),
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4106,8 +4158,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -4129,9 +4181,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4142,24 +4194,24 @@
                 new GenericDocument.Builder<>("namespace", "id1", "type")
                         .setPropertyString("body", "tablet")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id2", "type")
                         .setPropertyString("body", "tabby")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id3", "type")
                         .setPropertyString("body", "grabby")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we should get a failure
         GenericDocument document4 =
@@ -4172,8 +4224,8 @@
                                         "package",
                                         "database",
                                         document4,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4185,7 +4237,7 @@
                 "database",
                 "nothing",
                 new SearchSpec.Builder().build(),
-                /*removeStatsBuilder=*/ null);
+                /* removeStatsBuilder= */ null);
 
         // Should still fail
         e =
@@ -4196,8 +4248,8 @@
                                         "package",
                                         "database",
                                         document4,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4209,21 +4261,21 @@
                 "database",
                 "tab",
                 new SearchSpec.Builder().build(),
-                /*removeStatsBuilder=*/ null);
+                /* removeStatsBuilder= */ null);
 
         // Now doc4 and doc5 should work
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 document4,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id5", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // We only deleted 2 docs so the next one should fail again
         e =
@@ -4235,8 +4287,8 @@
                                         "database",
                                         new GenericDocument.Builder<>("namespace", "id6", "type")
                                                 .build(),
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4250,9 +4302,9 @@
                         IllegalArgumentException.class,
                         () ->
                                 mAppSearchImpl.removeByQuery(
-                                        /*packageName=*/ "",
-                                        /*databaseName=*/ "",
-                                        /*queryExpression=*/ "",
+                                        /* packageName= */ "",
+                                        /* databaseName= */ "",
+                                        /* queryExpression= */ "",
                                         new SearchSpec.Builder()
                                                 .setJoinSpec(
                                                         new JoinSpec.Builder("childProp").build())
@@ -4287,8 +4339,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -4304,9 +4356,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4317,8 +4369,8 @@
                 new GenericDocument.Builder<>("namespace", "id1", "type")
                         .setPropertyString("body", "id1.orig")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         // Replace it with another doc
         mAppSearchImpl.putDocument(
                 "package",
@@ -4326,16 +4378,16 @@
                 new GenericDocument.Builder<>("namespace", "id1", "type")
                         .setPropertyString("body", "id1.new")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Index id2. This should pass but only because we check for replacements.
         mAppSearchImpl.putDocument(
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we should get a failure on id3
         GenericDocument document3 =
@@ -4348,8 +4400,8 @@
                                         "package",
                                         "database",
                                         document3,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4382,8 +4434,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Insert schema
@@ -4399,9 +4451,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4412,8 +4464,8 @@
                 new GenericDocument.Builder<>("namespace", "id1", "type")
                         .setPropertyString("body", "id1.orig")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         // Replace it with another doc
         mAppSearchImpl.putDocument(
                 "package",
@@ -4421,8 +4473,8 @@
                 new GenericDocument.Builder<>("namespace", "id1", "type")
                         .setPropertyString("body", "id1.new")
                         .build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Reinitialize to make sure replacements are correctly accounted for by init
         mAppSearchImpl.close();
@@ -4447,8 +4499,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Index id2. This should pass but only because we check for replacements.
@@ -4456,8 +4508,8 @@
                 "package",
                 "database",
                 new GenericDocument.Builder<>("namespace", "id2", "type").build(),
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Now we should get a failure on id3
         GenericDocument document3 =
@@ -4470,8 +4522,8 @@
                                         "package",
                                         "database",
                                         document3,
-                                        /*sendChangeNotifications=*/ false,
-                                        /*logger=*/ null));
+                                        /* sendChangeNotifications= */ false,
+                                        /* logger= */ null));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_OUT_OF_SPACE);
         assertThat(e)
                 .hasMessageThat()
@@ -4503,8 +4555,8 @@
                                     }
                                 },
                                 new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         AppSearchException e =
@@ -4514,8 +4566,8 @@
                                 mAppSearchImpl.searchSuggestion(
                                         "package",
                                         "database",
-                                        /*suggestionQueryExpression=*/ "t",
-                                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 10)
+                                        /* suggestionQueryExpression= */ "t",
+                                        new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
                                                 .build()));
         assertThat(e.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
         assertThat(e)
@@ -4535,24 +4587,25 @@
                 mAppSearchImpl.setSchema(
                         mContext.getPackageName(),
                         "database1",
-                        /*schemas=*/ ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* schemas= */ ImmutableList.of(
+                                new AppSearchSchema.Builder("Type1").build()),
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer twice, on different packages.
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ fakePackage,
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ fakePackage,
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -4566,8 +4619,8 @@
                 mContext.getPackageName(),
                 "database1",
                 validDoc,
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
 
         // Dispatch notifications and empty the observers
         mAppSearchImpl.dispatchAndClearChangeNotifications();
@@ -4582,8 +4635,8 @@
                 mContext.getPackageName(),
                 "database1",
                 doc2,
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
 
         // Observer should still have received this data from its registration on
         // context.getPackageName(), as we only removed the copy from fakePackage.
@@ -4596,7 +4649,7 @@
                                 "database1",
                                 "namespace1",
                                 "Type1",
-                                /*changedDocumentIds=*/ ImmutableSet.of("id2")));
+                                /* changedDocumentIds= */ ImmutableSet.of("id2")));
     }
 
     @Test
@@ -4613,7 +4666,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -4622,9 +4675,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4635,8 +4688,8 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         AppSearchException e =
@@ -4648,8 +4701,8 @@
                                         "database",
                                         "namespace1",
                                         "id1",
-                                        /*typePropertyPaths=*/ Collections.emptyMap(),
-                                        /*callerAccess=*/ mSelfCallerAccess));
+                                        /* typePropertyPaths= */ Collections.emptyMap(),
+                                        /* callerAccess= */ mSelfCallerAccess));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
         assertThat(e.getMessage()).isEqualTo("Document (namespace1, id1) not found.");
     }
@@ -4668,7 +4721,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -4677,9 +4730,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4690,8 +4743,8 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         GenericDocument getResult =
@@ -4700,8 +4753,8 @@
                         "database",
                         "namespace1",
                         "id1",
-                        /*typePropertyPaths=*/ Collections.emptyMap(),
-                        /*callerAccess=*/ mSelfCallerAccess);
+                        /* typePropertyPaths= */ Collections.emptyMap(),
+                        /* callerAccess= */ mSelfCallerAccess);
         assertThat(getResult).isEqualTo(document);
     }
 
@@ -4719,7 +4772,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -4728,9 +4781,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4741,8 +4794,8 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         AppSearchException e =
@@ -4754,8 +4807,8 @@
                                         "database",
                                         "namespace1",
                                         "id2",
-                                        /*typePropertyPaths=*/ Collections.emptyMap(),
-                                        /*callerAccess=*/ mSelfCallerAccess));
+                                        /* typePropertyPaths= */ Collections.emptyMap(),
+                                        /* callerAccess= */ mSelfCallerAccess));
         assertThat(e.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
         assertThat(e.getMessage()).isEqualTo("Document (namespace1, id2) not found.");
     }
@@ -4789,7 +4842,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -4798,9 +4851,9 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -4811,8 +4864,8 @@
                 "package",
                 "database",
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
         mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
 
         AppSearchException unauthorizedException =
@@ -4824,12 +4877,12 @@
                                         "database",
                                         "namespace1",
                                         "id1",
-                                        /*typePropertyPaths=*/ Collections.emptyMap(),
+                                        /* typePropertyPaths= */ Collections.emptyMap(),
                                         new CallerAccess(
-                                                /*callingPackageName=*/ "invisiblePackage")));
+                                                /* callingPackageName= */ "invisiblePackage")));
 
         mAppSearchImpl.remove(
-                "package", "database", "namespace1", "id1", /*removeStatsBuilder=*/ null);
+                "package", "database", "namespace1", "id1", /* removeStatsBuilder= */ null);
 
         AppSearchException noDocException =
                 assertThrows(
@@ -4840,9 +4893,9 @@
                                         "database",
                                         "namespace1",
                                         "id1",
-                                        /*typePropertyPaths=*/ Collections.emptyMap(),
+                                        /* typePropertyPaths= */ Collections.emptyMap(),
                                         new CallerAccess(
-                                                /*callingPackageName=*/ "visiblePackage")));
+                                                /* callingPackageName= */ "visiblePackage")));
 
         assertThat(noDocException.getResultCode()).isEqualTo(unauthorizedException.getResultCode());
         assertThat(noDocException.getMessage()).isEqualTo(unauthorizedException.getMessage());
@@ -4864,9 +4917,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         String prefix = PrefixUtil.createPrefix("package", "database1");
@@ -4886,9 +4939,9 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix + "Email",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix + "Email",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
     }
 
@@ -4909,9 +4962,9 @@
                         "package1",
                         "database",
                         schemas1,
-                        /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig1),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(visibilityConfig1),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         String prefix1 = PrefixUtil.createPrefix("package1", "database");
@@ -4931,9 +4984,9 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix1 + "Email1",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix1 + "Email1",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
 
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
 
@@ -4952,9 +5005,9 @@
                         "package2",
                         "database",
                         schemas2,
-                        /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig2),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(visibilityConfig2),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         String prefix2 = PrefixUtil.createPrefix("package2", "database");
@@ -4974,9 +5027,9 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix2 + "Email2",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix2 + "Email2",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualDocument2).isEqualTo(expectedDocument2);
 
         // Check the existing visibility document retains.
@@ -4989,9 +5042,9 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix1 + "Email1",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix1 + "Email1",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
     }
 
@@ -5013,9 +5066,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         String prefix = PrefixUtil.createPrefix("package", "database1");
@@ -5032,9 +5085,9 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix + "Email",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix + "Email",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // Set schema Email and its all-default visibility document to AppSearch database1
@@ -5043,9 +5096,9 @@
                         "package",
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityConfigs= */ ImmutableList.of(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         // All-default visibility document won't be saved in AppSearch.
@@ -5059,8 +5112,8 @@
                                         VISIBILITY_PACKAGE_NAME,
                                         VISIBILITY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                        /*id=*/ prefix + "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ prefix + "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Document (VS#Pkg$VS#Db/, package$database1/Email) not found.");
@@ -5083,9 +5136,9 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ ImmutableList.of(visibilityConfig),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
         String prefix = PrefixUtil.createPrefix("package", "database1");
         InternalVisibilityConfig expectedDocument =
@@ -5102,19 +5155,19 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix + "Email",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix + "Email",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // remove the schema and visibility setting from AppSearch
         mAppSearchImpl.setSchema(
                 "package",
                 "database1",
-                /*schemas=*/ new ArrayList<>(),
-                /*visibilityConfigs=*/ ImmutableList.of(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* schemas= */ new ArrayList<>(),
+                /* visibilityConfigs= */ ImmutableList.of(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
 
         // add the schema back with an all default visibility setting.
@@ -5122,9 +5175,9 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityConfigs=*/ ImmutableList.of(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
+                /* visibilityConfigs= */ ImmutableList.of(),
+                /* forceOverride= */ false,
+                /* version= */ 0,
                 /* setSchemaStatsBuilder= */ null);
         // All-default visibility document won't be saved in AppSearch.
         assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
@@ -5137,8 +5190,8 @@
                                         VISIBILITY_PACKAGE_NAME,
                                         VISIBILITY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                        /*id=*/ prefix + "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ prefix + "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Document (VS#Pkg$VS#Db/, package$database1/Email) not found.");
@@ -5160,9 +5213,9 @@
                         "databaseName",
                         schemas,
                         ImmutableList.of(visibilityConfig),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // close and re-open AppSearchImpl, the visibility document retains
@@ -5172,8 +5225,8 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
@@ -5192,9 +5245,9 @@
                                 VISIBILITY_PACKAGE_NAME,
                                 VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix + "Email",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix + "Email",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // remove schema and visibility document
@@ -5204,9 +5257,9 @@
                         "databaseName",
                         ImmutableList.of(),
                         ImmutableList.of(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // close and re-open AppSearchImpl, the visibility document removed
@@ -5216,8 +5269,8 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
@@ -5230,8 +5283,8 @@
                                         VISIBILITY_PACKAGE_NAME,
                                         VISIBILITY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                        /*id=*/ prefix + "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ prefix + "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e)
                 .hasMessageThat()
                 .contains("Document (VS#Pkg$VS#Db/, packageName$databaseName/Email) not found.");
@@ -5251,7 +5304,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5261,13 +5314,13 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(
+                        /* visibilityConfigs= */ ImmutableList.of(
                                 new InternalVisibilityConfig.Builder("Type")
                                         .setNotDisplayedBySystem(true)
                                         .build()),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Get this schema as another package
@@ -5276,7 +5329,7 @@
                         "package",
                         "database",
                         new CallerAccess(
-                                /*callingPackageName=*/ "com.android.appsearch.fake.package"));
+                                /* callingPackageName= */ "com.android.appsearch.fake.package"));
         assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("Type");
     }
@@ -5289,10 +5342,10 @@
                         "package",
                         "database",
                         Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
-                        /*visibilityConfigs=*/ ImmutableList.of(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ ImmutableList.of(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Try to get the schema of a nonexistent package.
@@ -5300,7 +5353,7 @@
                 mAppSearchImpl.getSchema(
                         "com.android.appsearch.fake.package",
                         "database",
-                        new CallerAccess(/*callingPackageName=*/ "package"));
+                        new CallerAccess(/* callingPackageName= */ "package"));
         assertThat(getResponse.getSchemas()).isEmpty();
         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
     }
@@ -5315,17 +5368,17 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ ImmutableList.of(),
+                        /* forceOverride= */ false,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         GetSchemaResponse getResponse =
                 mAppSearchImpl.getSchema(
                         "package",
                         "database",
                         new CallerAccess(
-                                /*callingPackageName=*/ "com.android.appsearch.fake.package"));
+                                /* callingPackageName= */ "com.android.appsearch.fake.package"));
         assertThat(getResponse.getSchemas()).isEmpty();
         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).isEmpty();
         assertThat(getResponse.getVersion()).isEqualTo(0);
@@ -5334,7 +5387,9 @@
         // from the same package
         getResponse =
                 mAppSearchImpl.getSchema(
-                        "package", "database", new CallerAccess(/*callingPackageName=*/ "package"));
+                        "package",
+                        "database",
+                        new CallerAccess(/* callingPackageName= */ "package"));
         assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
     }
 
@@ -5370,7 +5425,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         mockVisibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5380,16 +5435,16 @@
                         "package",
                         "database",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(
+                        /* visibilityConfigs= */ ImmutableList.of(
                                 new InternalVisibilityConfig.Builder("VisibleType")
                                         .setNotDisplayedBySystem(true)
                                         .build(),
                                 new InternalVisibilityConfig.Builder("PrivateType")
                                         .setNotDisplayedBySystem(true)
                                         .build()),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ false,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         GetSchemaResponse getResponse =
@@ -5397,7 +5452,7 @@
                         "package",
                         "database",
                         new CallerAccess(
-                                /*callingPackageName=*/ "com.android.appsearch.fake.package"));
+                                /* callingPackageName= */ "com.android.appsearch.fake.package"));
         assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
         assertThat(getResponse.getSchemaTypesNotDisplayedBySystem()).containsExactly("VisibleType");
         assertThat(getResponse.getVersion()).isEqualTo(1);
@@ -5455,7 +5510,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         publicAclMockChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5478,9 +5533,9 @@
                         "database",
                         schemas,
                         visibilityConfigs,
-                        /*forceOverride=*/ true,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ true,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Verify access to schemas based on calling package
@@ -5560,7 +5615,7 @@
                         tempFolder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         publicAclMockChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5583,9 +5638,9 @@
                         "database",
                         schemas,
                         visibilityConfigs,
-                        /*forceOverride=*/ true,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ true,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Now check for documents
@@ -5668,9 +5723,9 @@
                         "database",
                         schemas,
                         visibilityConfigs,
-                        /*forceOverride=*/ true,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ true,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponseRemoved.isSuccess()).isTrue();
 
         // Now check for documents again
@@ -5717,17 +5772,17 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -5739,8 +5794,8 @@
                 mContext.getPackageName(),
                 "database1",
                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
 
@@ -5767,7 +5822,7 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         rejectChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5777,17 +5832,17 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -5799,8 +5854,8 @@
                 mContext.getPackageName(),
                 "database1",
                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
 
@@ -5825,17 +5880,17 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer from a simulated different package
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ "com.fake.Listening.package"),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ "com.fake.Listening.package"),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -5847,8 +5902,8 @@
                 mContext.getPackageName(),
                 "database1",
                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
 
@@ -5885,7 +5940,7 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         visibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5895,17 +5950,17 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakeListeningPackage),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -5917,8 +5972,8 @@
                 mContext.getPackageName(),
                 "database1",
                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
 
@@ -5947,7 +6002,7 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         rejectChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -5957,17 +6012,17 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakeListeningPackage),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -5979,8 +6034,8 @@
                 mContext.getPackageName(),
                 "database1",
                 new GenericDocument.Builder<>("namespace1", "id1", "Type1").build(),
-                /*sendChangeNotifications=*/ true,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ true,
+                /* logger= */ null);
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
 
@@ -5995,8 +6050,8 @@
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6009,10 +6064,10 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
@@ -6035,10 +6090,10 @@
                                 new AppSearchSchema.Builder("Type1").build(),
                                 new AppSearchSchema.Builder("Type2").build(),
                                 new AppSearchSchema.Builder("Type3").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
@@ -6064,17 +6119,17 @@
                         ImmutableList.of(
                                 new AppSearchSchema.Builder("Type1").build(),
                                 new AppSearchSchema.Builder("Type2").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6085,10 +6140,10 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications
@@ -6120,17 +6175,17 @@
                                                                         .CARDINALITY_REQUIRED)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6151,10 +6206,10 @@
                                                                         .CARDINALITY_REQUIRED)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications
@@ -6181,10 +6236,10 @@
                                                                         .CARDINALITY_OPTIONAL)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 2,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 2,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications
@@ -6224,17 +6279,17 @@
                                                                         .CARDINALITY_REQUIRED)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer that only listens for Type2
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                /*listeningPackageAccess=*/ mSelfCallerAccess,
-                /*targetPackageName=*/ mContext.getPackageName(),
+                /* listeningPackageAccess= */ mSelfCallerAccess,
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas("Type2").build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6263,10 +6318,10 @@
                                                                         .CARDINALITY_OPTIONAL)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications
@@ -6320,15 +6375,15 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         visibilityChecker,
                         ALWAYS_OPTIMIZE);
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakeListeningPackage),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6343,7 +6398,7 @@
                         mContext.getPackageName(),
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(
+                        /* visibilityConfigs= */ ImmutableList.of(
                                 new InternalVisibilityConfig.Builder("Type1")
                                         .addVisibleToPackage(
                                                 new PackageIdentifier(
@@ -6354,9 +6409,9 @@
                                                 new PackageIdentifier(
                                                         fakeListeningPackage, new byte[0]))
                                         .build()),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Notifications of addition should now be dispatched
@@ -6378,16 +6433,16 @@
                         mContext.getPackageName(),
                         "database1",
                         schemas,
-                        /*visibilityConfigs=*/ ImmutableList.of(
+                        /* visibilityConfigs= */ ImmutableList.of(
                                 new InternalVisibilityConfig.Builder("Type1")
                                         .addVisibleToPackage(
                                                 new PackageIdentifier(
                                                         fakeListeningPackage, new byte[0]))
                                         .build(),
                                 new InternalVisibilityConfig.Builder("Type2").build()),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications. This should look like a deletion of Type2.
@@ -6417,16 +6472,16 @@
                                                                         .CARDINALITY_OPTIONAL)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ ImmutableList.of(
+                        /* visibilityConfigs= */ ImmutableList.of(
                                 new InternalVisibilityConfig.Builder("Type1")
                                         .addVisibleToPackage(
                                                 new PackageIdentifier(
                                                         fakeListeningPackage, new byte[0]))
                                         .build(),
                                 new InternalVisibilityConfig.Builder("Type2").build()),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         assertThat(observer.getSchemaChanges()).isEmpty();
@@ -6451,7 +6506,7 @@
                                                                         .CARDINALITY_OPTIONAL)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ ImmutableList.of(
+                        /* visibilityConfigs= */ ImmutableList.of(
                                 new InternalVisibilityConfig.Builder("Type1")
                                         .addVisibleToPackage(
                                                 new PackageIdentifier(
@@ -6462,9 +6517,9 @@
                                                 new PackageIdentifier(
                                                         fakeListeningPackage, new byte[0]))
                                         .build()),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications. This should look like a creation of Type2.
@@ -6506,7 +6561,7 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         visibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -6534,17 +6589,17 @@
                                                                         .CARDINALITY_REQUIRED)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakeListeningPackage),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6573,10 +6628,10 @@
                                                                         .CARDINALITY_OPTIONAL)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications
@@ -6618,7 +6673,7 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         visibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -6630,17 +6685,17 @@
                         ImmutableList.of(
                                 new AppSearchSchema.Builder("Type1").build(),
                                 new AppSearchSchema.Builder("Type2").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakeListeningPackage),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakeListeningPackage),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6651,10 +6706,10 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications. Nothing should appear since Type1 is not visible to us.
@@ -6670,10 +6725,10 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
@@ -6725,7 +6780,7 @@
                         mAppSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
+                        /* initStatsBuilder= */ null,
                         visibilityChecker,
                         ALWAYS_OPTIMIZE);
 
@@ -6739,33 +6794,33 @@
                                 new AppSearchSchema.Builder("Type2").build(),
                                 new AppSearchSchema.Builder("Type3").build(),
                                 new AppSearchSchema.Builder("Type4").build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register three observers: one in each package, and another in package1 with a filter.
         TestObserverCallback observerPkg1NoFilter = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakePackage1),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakePackage1),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observerPkg1NoFilter);
 
         TestObserverCallback observerPkg2NoFilter = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakePackage2),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakePackage2),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observerPkg2NoFilter);
 
         TestObserverCallback observerPkg1FilterType4 = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ fakePackage1),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ fakePackage1),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().addFilterSchemas("Type4").build(),
                 MoreExecutors.directExecutor(),
                 observerPkg1FilterType4);
@@ -6776,10 +6831,10 @@
                         mContext.getPackageName(),
                         "database1",
                         ImmutableList.of(),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Dispatch notifications.
@@ -6837,17 +6892,17 @@
                                                                         .CARDINALITY_OPTIONAL)
                                                         .build())
                                         .build()),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
-                new CallerAccess(/*callingPackageName=*/ mContext.getPackageName()),
-                /*targetPackageName=*/ mContext.getPackageName(),
+                new CallerAccess(/* callingPackageName= */ mContext.getPackageName()),
+                /* targetPackageName= */ mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
                 observer);
@@ -6877,10 +6932,10 @@
                         mContext.getPackageName(),
                         "database1",
                         updatedSchemaTypes,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 2,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 2,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isFalse();
         SetSchemaResponse setSchemaResponse = internalSetSchemaResponse.getSetSchemaResponse();
         assertThat(setSchemaResponse.getDeletedTypes()).isEmpty();
@@ -6900,10 +6955,10 @@
                         mContext.getPackageName(),
                         "database1",
                         updatedSchemaTypes,
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ 3,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ 3,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         assertThat(observer.getSchemaChanges()).isEmpty();
         assertThat(observer.getDocumentChanges()).isEmpty();
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchLoggerTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchLoggerTest.java
index 08fa4e5..fd17b4c 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchLoggerTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/AppSearchLoggerTest.java
@@ -63,6 +63,7 @@
 public class AppSearchLoggerTest {
     private static final String PACKAGE_NAME = "packageName";
     private static final String DATABASE = "database";
+
     /**
      * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
      */
@@ -79,8 +80,8 @@
                         mTemporaryFolder.newFolder(),
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         mLogger = new SimpleTestLogger();
     }
@@ -378,7 +379,7 @@
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
                         initStatsBuilder,
-                        /*visibilityChecker=*/ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
         appSearchImpl.close();
@@ -409,8 +410,8 @@
                         folder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         List<AppSearchSchema> schemas =
                 ImmutableList.of(
@@ -421,17 +422,17 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type1").build();
         GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type1").build();
         appSearchImpl.putDocument(
-                testPackageName, testDatabase, doc1, /*sendChangeNotifications=*/ false, mLogger);
+                testPackageName, testDatabase, doc1, /* sendChangeNotifications= */ false, mLogger);
         appSearchImpl.putDocument(
-                testPackageName, testDatabase, doc2, /*sendChangeNotifications=*/ false, mLogger);
+                testPackageName, testDatabase, doc2, /* sendChangeNotifications= */ false, mLogger);
         appSearchImpl.close();
 
         // Create another appsearchImpl on the same folder
@@ -442,7 +443,7 @@
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
                         initStatsBuilder,
-                        /*visibilityChecker=*/ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
 
@@ -474,8 +475,8 @@
                         folder,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         List<AppSearchSchema> schemas =
@@ -487,16 +488,16 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Insert a valid doc
         GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type1").build();
         appSearchImpl.putDocument(
-                testPackageName, testDatabase, doc1, /*sendChangeNotifications=*/ false, mLogger);
+                testPackageName, testDatabase, doc1, /* sendChangeNotifications= */ false, mLogger);
 
         // Insert the invalid doc with an invalid namespace right into icing
         DocumentProto invalidDoc =
@@ -517,7 +518,7 @@
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
                         initStatsBuilder,
-                        /*visibilityChecker=*/ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
 
@@ -553,9 +554,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -568,7 +569,7 @@
                 testPackageName,
                 testDatabase,
                 document,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
 
         PutDocumentStats pStats = mLogger.mPutDocumentStats;
@@ -606,9 +607,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -625,7 +626,7 @@
                                         testPackageName,
                                         testDatabase,
                                         document,
-                                        /*sendChangeNotifications=*/ false,
+                                        /* sendChangeNotifications= */ false,
                                         mLogger));
         assertThat(exception.getResultCode()).isEqualTo(AppSearchResult.RESULT_NOT_FOUND);
 
@@ -661,9 +662,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         GenericDocument document1 =
@@ -682,19 +683,19 @@
                 testPackageName,
                 testDatabase,
                 document1,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 document2,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 document3,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
 
         // No query filters specified. package2 should only get its own documents back.
@@ -706,7 +707,7 @@
         String queryStr = "testPut e";
         SearchResultPage searchResultPage =
                 mAppSearchImpl.query(
-                        testPackageName, testDatabase, queryStr, searchSpec, /*logger=*/ mLogger);
+                        testPackageName, testDatabase, queryStr, searchSpec, /* logger= */ mLogger);
 
         assertThat(searchResultPage.getResults()).hasSize(2);
         // The ranking strategy is LIFO
@@ -747,9 +748,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -765,7 +766,7 @@
                 testPackageName,
                 /* queryExpression= */ "",
                 searchSpec,
-                /*logger=*/ mLogger);
+                /* logger= */ mLogger);
 
         SearchStats sStats = mLogger.mSearchStats;
         assertThat(sStats).isNotNull();
@@ -818,9 +819,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
 
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
@@ -854,37 +855,37 @@
                 testPackageName,
                 testDatabase,
                 entity1,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 entity2,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 action1,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 action2,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 action3,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 action4,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
 
         SearchSpec nestedSearchSpec =
@@ -909,7 +910,7 @@
         String queryStr = "entity";
         SearchResultPage searchResultPage =
                 mAppSearchImpl.query(
-                        testPackageName, testDatabase, queryStr, searchSpec, /*logger=*/ mLogger);
+                        testPackageName, testDatabase, queryStr, searchSpec, /* logger= */ mLogger);
 
         assertThat(searchResultPage.getResults()).hasSize(2);
         assertThat(searchResultPage.getResults().get(0).getGenericDocument()).isEqualTo(entity1);
@@ -955,9 +956,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         GenericDocument document =
@@ -966,8 +967,8 @@
                 testPackageName,
                 testDatabase,
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         RemoveStats.Builder rStatsBuilder = new RemoveStats.Builder(testPackageName, testDatabase);
         mAppSearchImpl.remove(testPackageName, testDatabase, testNamespace, testId, rStatsBuilder);
@@ -995,9 +996,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1007,8 +1008,8 @@
                 testPackageName,
                 testDatabase,
                 document,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         RemoveStats.Builder rStatsBuilder = new RemoveStats.Builder(testPackageName, testDatabase);
 
@@ -1047,9 +1048,9 @@
                         testPackageName,
                         testDatabase,
                         schemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         GenericDocument document1 =
@@ -1060,13 +1061,13 @@
                 testPackageName,
                 testDatabase,
                 document1,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         mAppSearchImpl.putDocument(
                 testPackageName,
                 testDatabase,
                 document2,
-                /*sendChangeNotifications=*/ false,
+                /* sendChangeNotifications= */ false,
                 mLogger);
         // No query filters specified. package2 should only get its own documents back.
         SearchSpec searchSpec =
@@ -1074,7 +1075,11 @@
 
         RemoveStats.Builder rStatsBuilder = new RemoveStats.Builder(testPackageName, testDatabase);
         mAppSearchImpl.removeByQuery(
-                testPackageName, testDatabase, /*queryExpression=*/ "", searchSpec, rStatsBuilder);
+                testPackageName,
+                testDatabase,
+                /* queryExpression= */ "",
+                searchSpec,
+                rStatsBuilder);
         RemoveStats rStats = rStatsBuilder.build();
 
         assertThat(rStats.getPackageName()).isEqualTo(testPackageName);
@@ -1107,9 +1112,9 @@
                         PACKAGE_NAME,
                         DATABASE,
                         Collections.singletonList(schema1),
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
@@ -1121,9 +1126,9 @@
                         PACKAGE_NAME,
                         DATABASE,
                         Collections.singletonList(schema2),
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*version=*/ 0,
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* version= */ 0,
                         /* setSchemaStatsBuilder= */ sStatsBuilder);
         assertThat(internalSetSchemaResponse.isSuccess()).isFalse();
 
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/SchemaCacheTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/SchemaCacheTest.java
new file mode 100644
index 0000000..aba9342
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/SchemaCacheTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage;
+
+import static com.android.server.appsearch.external.localstorage.util.PrefixUtil.createPrefix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.appsearch.icing.proto.SchemaTypeConfigProto;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public class SchemaCacheTest {
+
+    @Test
+    public void testGetSchemaTypesWithDescendants() throws Exception {
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Person").build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Other").build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(
+                        prefix,
+                        ImmutableMap.of(
+                                "package$database/Person", personSchema,
+                                "package$database/Artist", artistSchema,
+                                "package$database/Other", otherSchema));
+        SchemaCache schemaCache = new SchemaCache(schemaMap);
+
+        assertThat(
+                        schemaCache.getSchemaTypesWithDescendants(
+                                prefix, ImmutableSet.of("package$database/Person")))
+                .containsExactly("package$database/Person", "package$database/Artist");
+        assertThat(
+                        schemaCache.getSchemaTypesWithDescendants(
+                                prefix, ImmutableSet.of("package$database/Artist")))
+                .containsExactly("package$database/Artist");
+        assertThat(
+                        schemaCache.getSchemaTypesWithDescendants(
+                                prefix, ImmutableSet.of("package$database/Other")))
+                .containsExactly("package$database/Other");
+    }
+
+    @Test
+    public void testGetSchemaTypesWithDescendants_multipleLevel() throws Exception {
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto schemaA =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/A").build();
+        SchemaTypeConfigProto schemaB =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/B").build();
+        SchemaTypeConfigProto schemaC =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/C")
+                        .addParentTypes("package$database/A")
+                        .build();
+        SchemaTypeConfigProto schemaD =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/D")
+                        .addParentTypes("package$database/C")
+                        .build();
+        SchemaTypeConfigProto schemaE =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/E")
+                        .addParentTypes("package$database/B")
+                        .addParentTypes("package$database/C")
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(
+                        prefix,
+                        ImmutableMap.of(
+                                "package$database/A", schemaA,
+                                "package$database/B", schemaB,
+                                "package$database/C", schemaC,
+                                "package$database/D", schemaD,
+                                "package$database/E", schemaE));
+        SchemaCache schemaCache = new SchemaCache(schemaMap);
+
+        assertThat(
+                        schemaCache.getSchemaTypesWithDescendants(
+                                prefix, ImmutableSet.of("package$database/A")))
+                .containsExactly(
+                        "package$database/A",
+                        "package$database/C",
+                        "package$database/D",
+                        "package$database/E");
+        assertThat(
+                        schemaCache.getSchemaTypesWithDescendants(
+                                prefix, ImmutableSet.of("package$database/B")))
+                .containsExactly("package$database/B", "package$database/E");
+        assertThat(
+                        schemaCache.getSchemaTypesWithDescendants(
+                                prefix,
+                                ImmutableSet.of("package$database/A", "package$database/B")))
+                .containsExactly(
+                        "package$database/A",
+                        "package$database/B",
+                        "package$database/C",
+                        "package$database/D",
+                        "package$database/E");
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 84f52e1..3fe175f 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.appsearch.EmbeddingVector;
 import android.app.appsearch.GenericDocument;
 
 import com.android.server.appsearch.external.localstorage.AppSearchConfigImpl;
@@ -34,6 +35,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -45,6 +47,10 @@
     private static final byte[] BYTE_ARRAY_2 = new byte[] {(byte) 4, (byte) 5, (byte) 6, (byte) 7};
     private static final String SCHEMA_TYPE_1 = "sDocumentPropertiesSchemaType1";
     private static final String SCHEMA_TYPE_2 = "sDocumentPropertiesSchemaType2";
+    private static final EmbeddingVector sEmbedding1 =
+            new EmbeddingVector(new float[] {1.1f, 2.2f, 3.3f}, "my_model_v1");
+    private static final EmbeddingVector sEmbedding2 =
+            new EmbeddingVector(new float[] {4.4f, 5.5f, 6.6f, 7.7f}, "my_model_v2");
     private static final GenericDocument DOCUMENT_PROPERTIES_1 =
             new GenericDocument.Builder<GenericDocument.Builder<?>>(
                             "namespace", "sDocumentProperties1", SCHEMA_TYPE_1)
@@ -392,4 +398,69 @@
         assertThat(convertedDocumentProto).isEqualTo(outerDocumentProto);
         assertThat(convertedGenericDocument).isEqualTo(outerDocument);
     }
+
+    @Test
+    public void testDocumentProtoConvert_EmbeddingProperty() throws Exception {
+        GenericDocument document =
+                new GenericDocument.Builder<GenericDocument.Builder<?>>(
+                                "namespace", "id1", SCHEMA_TYPE_1)
+                        .setCreationTimestampMillis(5L)
+                        .setScore(1)
+                        .setTtlMillis(1L)
+                        .setPropertyLong("longKey1", 1L)
+                        .setPropertyDocument("documentKey1", DOCUMENT_PROPERTIES_1)
+                        .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                        .build();
+
+        // Create the Document proto. Need to sort the property order by key.
+        DocumentProto.Builder documentProtoBuilder =
+                DocumentProto.newBuilder()
+                        .setUri("id1")
+                        .setSchema(SCHEMA_TYPE_1)
+                        .setCreationTimestampMs(5L)
+                        .setScore(1)
+                        .setTtlMs(1L)
+                        .setNamespace("namespace");
+        HashMap<String, PropertyProto.Builder> propertyProtoMap = new HashMap<>();
+        propertyProtoMap.put(
+                "longKey1", PropertyProto.newBuilder().setName("longKey1").addInt64Values(1L));
+        propertyProtoMap.put(
+                "documentKey1",
+                PropertyProto.newBuilder()
+                        .setName("documentKey1")
+                        .addDocumentValues(
+                                GenericDocumentToProtoConverter.toDocumentProto(
+                                        DOCUMENT_PROPERTIES_1)));
+        propertyProtoMap.put(
+                "embeddingKey1",
+                PropertyProto.newBuilder()
+                        .setName("embeddingKey1")
+                        .addVectorValues(
+                                PropertyProto.VectorProto.newBuilder()
+                                        .addAllValues(Arrays.asList(1.1f, 2.2f, 3.3f))
+                                        .setModelSignature("my_model_v1"))
+                        .addVectorValues(
+                                PropertyProto.VectorProto.newBuilder()
+                                        .addAllValues(Arrays.asList(4.4f, 5.5f, 6.6f, 7.7f))
+                                        .setModelSignature("my_model_v2")));
+        List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
+        Collections.sort(sortedKey);
+        for (String key : sortedKey) {
+            documentProtoBuilder.addProperties(propertyProtoMap.get(key));
+        }
+        DocumentProto documentProto = documentProtoBuilder.build();
+
+        GenericDocument convertedGenericDocument =
+                GenericDocumentToProtoConverter.toGenericDocument(
+                        documentProto,
+                        PREFIX,
+                        SCHEMA_MAP,
+                        new AppSearchConfigImpl(
+                                new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()));
+        DocumentProto convertedDocumentProto =
+                GenericDocumentToProtoConverter.toDocumentProto(document);
+
+        assertThat(convertedDocumentProto).isEqualTo(documentProto);
+        assertThat(convertedGenericDocument).isEqualTo(document);
+    }
 }
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java
index 788bf98..9d54f3f 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SchemaToProtoConverterTest.java
@@ -21,6 +21,7 @@
 import android.app.appsearch.AppSearchSchema;
 
 import com.android.server.appsearch.icing.proto.DocumentIndexingConfig;
+import com.android.server.appsearch.icing.proto.EmbeddingIndexingConfig;
 import com.android.server.appsearch.icing.proto.JoinableConfig;
 import com.android.server.appsearch.icing.proto.PropertyConfigProto;
 import com.android.server.appsearch.icing.proto.SchemaTypeConfigProto;
@@ -33,6 +34,130 @@
 
 public class SchemaToProtoConverterTest {
     @Test
+    public void testGetProto_DescriptionSet() {
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setDescription("The most important part.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("timestamp")
+                                        .setDescription("The time at which the email was sent.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DoublePropertyConfig.Builder("importanceScore")
+                                        .setDescription(
+                                                "A value representing this document's importance.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BooleanPropertyConfig.Builder("read")
+                                        .setDescription(
+                                                "Whether the email has been read by the recipient")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BytesPropertyConfig.Builder("attachment")
+                                        .setDescription("Documents that are attached to the email.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .build())
+                        // We don't need to actually define the Person type for this test because
+                        // the converter will process each schema individually.
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                                "sender", "Person")
+                                        .setDescription("The person who wrote this email.")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .build();
+
+        SchemaTypeConfigProto expectedEmailProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("Email")
+                        .setDescription("A type of electronic message.")
+                        .setVersion(12345)
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("subject")
+                                        .setDescription("The most important part.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setStringIndexingConfig(
+                                                StringIndexingConfig.newBuilder()
+                                                        .setTokenizerType(
+                                                                StringIndexingConfig.TokenizerType
+                                                                        .Code.PLAIN)
+                                                        .setTermMatchType(
+                                                                TermMatchType.Code.PREFIX)))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("timestamp")
+                                        .setDescription("The time at which the email was sent.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.INT64)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("importanceScore")
+                                        .setDescription(
+                                                "A value representing this document's importance.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.DOUBLE)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("read")
+                                        .setDescription(
+                                                "Whether the email has been read by the recipient")
+                                        .setDataType(PropertyConfigProto.DataType.Code.BOOLEAN)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("attachment")
+                                        .setDescription("Documents that are attached to the email.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.BYTES)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.REPEATED))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("sender")
+                                        .setSchemaType("Person")
+                                        .setDescription("The person who wrote this email.")
+                                        .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setDocumentIndexingConfig(
+                                                DocumentIndexingConfig.newBuilder()
+                                                        .setIndexNestedProperties(false)))
+                        .build();
+
+        assertThat(
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(
+                                emailSchema, /* version= */ 12345))
+                .isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+                .isEqualTo(emailSchema);
+    }
+
+    @Test
     public void testGetProto_Email() {
         AppSearchSchema emailSchema =
                 new AppSearchSchema.Builder("Email")
@@ -63,10 +188,12 @@
         SchemaTypeConfigProto expectedEmailProto =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType("Email")
+                        .setDescription("")
                         .setVersion(12345)
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("subject")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                         .setCardinality(
                                                 PropertyConfigProto.Cardinality.Code.OPTIONAL)
@@ -80,6 +207,7 @@
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("body")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                         .setCardinality(
                                                 PropertyConfigProto.Cardinality.Code.OPTIONAL)
@@ -92,7 +220,9 @@
                                                                 TermMatchType.Code.PREFIX)))
                         .build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema, /*version=*/ 12345))
+        assertThat(
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(
+                                emailSchema, /* version= */ 12345))
                 .isEqualTo(expectedEmailProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
                 .isEqualTo(emailSchema);
@@ -123,10 +253,12 @@
         SchemaTypeConfigProto expectedMusicRecordingProto =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType("MusicRecording")
+                        .setDescription("")
                         .setVersion(0)
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("artist")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                         .setCardinality(
                                                 PropertyConfigProto.Cardinality.Code.REPEATED)
@@ -140,6 +272,7 @@
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("pubDate")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.INT64)
                                         .setCardinality(
                                                 PropertyConfigProto.Cardinality.Code.OPTIONAL))
@@ -147,7 +280,7 @@
 
         assertThat(
                         SchemaToProtoConverter.toSchemaTypeConfigProto(
-                                musicRecordingSchema, /*version=*/ 0))
+                                musicRecordingSchema, /* version= */ 0))
                 .isEqualTo(expectedMusicRecordingProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
                 .isEqualTo(musicRecordingSchema);
@@ -164,10 +297,6 @@
                                         .setJoinableValueType(
                                                 AppSearchSchema.StringPropertyConfig
                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        // TODO(b/274157614): Export this to framework when we can
-                                        // access hidden
-                                        //  APIs.
-
                                         .build())
                         .build();
 
@@ -179,10 +308,12 @@
         SchemaTypeConfigProto expectedAlbumProto =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType("Album")
+                        .setDescription("")
                         .setVersion(0)
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("artist")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                         .setCardinality(
                                                 PropertyConfigProto.Cardinality.Code.OPTIONAL)
@@ -196,7 +327,7 @@
                                         .setJoinableConfig(joinableConfig))
                         .build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(albumSchema, /*version=*/ 0))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(albumSchema, /* version= */ 0))
                 .isEqualTo(expectedAlbumProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedAlbumProto))
                 .isEqualTo(albumSchema);
@@ -213,12 +344,13 @@
         SchemaTypeConfigProto expectedSchemaProto =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType("EmailMessage")
+                        .setDescription("")
                         .setVersion(12345)
                         .addParentTypes("Email")
                         .addParentTypes("Message")
                         .build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(schema, /*version=*/ 12345))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(schema, /* version= */ 12345))
                 .isEqualTo(expectedSchemaProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedSchemaProto)).isEqualTo(schema);
     }
@@ -259,10 +391,12 @@
         SchemaTypeConfigProto expectedPersonProto =
                 SchemaTypeConfigProto.newBuilder()
                         .setSchemaType("Person")
+                        .setDescription("")
                         .setVersion(0)
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("name")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                                         .setCardinality(
                                                 PropertyConfigProto.Cardinality.Code.REQUIRED)
@@ -275,6 +409,7 @@
                         .addProperties(
                                 PropertyConfigProto.newBuilder()
                                         .setPropertyName("worksFor")
+                                        .setDescription("")
                                         .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
                                         .setSchemaType("Organization")
                                         .setCardinality(
@@ -282,9 +417,117 @@
                                         .setDocumentIndexingConfig(documentIndexingConfig))
                         .build();
 
-        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(personSchema, /*version=*/ 0))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(personSchema, /* version= */ 0))
                 .isEqualTo(expectedPersonProto);
         assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedPersonProto))
                 .isEqualTo(personSchema);
     }
+
+    @Test
+    public void testGetProto_EmbeddingProperty() {
+        AppSearchSchema emailSchema =
+                new AppSearchSchema.Builder("Email")
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.StringPropertyConfig.Builder("body")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                AppSearchSchema.StringPropertyConfig
+                                                        .TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.EmbeddingPropertyConfig
+                                                        .INDEXING_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.EmbeddingPropertyConfig.Builder(
+                                                "indexableEmbedding")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.EmbeddingPropertyConfig
+                                                        .INDEXING_TYPE_SIMILARITY)
+                                        .build())
+                        .build();
+
+        SchemaTypeConfigProto expectedEmailProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("Email")
+                        .setDescription("")
+                        .setVersion(12345)
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("subject")
+                                        .setDescription("")
+                                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setStringIndexingConfig(
+                                                StringIndexingConfig.newBuilder()
+                                                        .setTokenizerType(
+                                                                StringIndexingConfig.TokenizerType
+                                                                        .Code.PLAIN)
+                                                        .setTermMatchType(
+                                                                TermMatchType.Code.PREFIX)))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("body")
+                                        .setDescription("")
+                                        .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setStringIndexingConfig(
+                                                StringIndexingConfig.newBuilder()
+                                                        .setTokenizerType(
+                                                                StringIndexingConfig.TokenizerType
+                                                                        .Code.PLAIN)
+                                                        .setTermMatchType(
+                                                                TermMatchType.Code.PREFIX)))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("embedding")
+                                        .setDescription("")
+                                        .setDataType(PropertyConfigProto.DataType.Code.VECTOR)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL))
+                        .addProperties(
+                                PropertyConfigProto.newBuilder()
+                                        .setPropertyName("indexableEmbedding")
+                                        .setDescription("")
+                                        .setDataType(PropertyConfigProto.DataType.Code.VECTOR)
+                                        .setCardinality(
+                                                PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                                        .setEmbeddingIndexingConfig(
+                                                EmbeddingIndexingConfig.newBuilder()
+                                                        .setEmbeddingIndexingType(
+                                                                EmbeddingIndexingConfig
+                                                                        .EmbeddingIndexingType.Code
+                                                                        .LINEAR_SEARCH)))
+                        .build();
+
+        assertThat(
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(
+                                emailSchema, /* version= */ 12345))
+                .isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+                .isEqualTo(emailSchema);
+    }
 }
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverterTest.java
index ffcf826..2640672 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -28,6 +28,7 @@
 
 import com.android.server.appsearch.external.localstorage.AppSearchConfigImpl;
 import com.android.server.appsearch.external.localstorage.LocalStorageIcingOptionsConfig;
+import com.android.server.appsearch.external.localstorage.SchemaCache;
 import com.android.server.appsearch.external.localstorage.UnlimitedLimitConfig;
 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
 import com.android.server.appsearch.icing.proto.DocumentProto;
@@ -89,7 +90,7 @@
         removePrefixesFromDocument(joinedDocProtoBuilder);
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(
-                        searchResultProto, schemaMap, config);
+                        searchResultProto, new SchemaCache(schemaMap), config);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult result = searchResultPage.getResults().get(0);
         assertThat(result.getPackageName()).isEqualTo("com.package.foo");
@@ -166,7 +167,7 @@
                         () ->
                                 SearchResultToProtoConverter.toSearchResultPage(
                                         searchResultProto,
-                                        schemaMap,
+                                        new SchemaCache(schemaMap),
                                         new AppSearchConfigImpl(
                                                 new UnlimitedLimitConfig(),
                                                 new LocalStorageIcingOptionsConfig())));
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java
index a2eda8a..2f717d3 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -33,6 +33,7 @@
 import com.android.server.appsearch.external.localstorage.IcingOptionsConfig;
 import com.android.server.appsearch.external.localstorage.LocalStorageIcingOptionsConfig;
 import com.android.server.appsearch.external.localstorage.OptimizeStrategy;
+import com.android.server.appsearch.external.localstorage.SchemaCache;
 import com.android.server.appsearch.external.localstorage.UnlimitedLimitConfig;
 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
 import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
@@ -77,8 +78,8 @@
                         mTemporaryFolder.newFolder(),
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), mLocalStorageIcingOptionsConfig),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
     }
 
@@ -96,25 +97,26 @@
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                         ImmutableSet.of(
                                                 prefix1 + "namespace1", prefix1 + "namespace2"),
                                 prefix2,
                                         ImmutableSet.of(
                                                 prefix2 + "namespace1", prefix2 + "namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                        ImmutableMap.of(
-                                                prefix1 + "typeA", configProto,
-                                                prefix1 + "typeB", configProto),
-                                prefix2,
-                                        ImmutableMap.of(
-                                                prefix2 + "typeA", configProto,
-                                                prefix2 + "typeB", configProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                                ImmutableMap.of(
+                                                        prefix1 + "typeA", configProto,
+                                                        prefix1 + "typeB", configProto),
+                                        prefix2,
+                                                ImmutableMap.of(
+                                                        prefix2 + "typeA", configProto,
+                                                        prefix2 + "typeB", configProto))),
                         mLocalStorageIcingOptionsConfig);
         // Convert SearchSpec to proto.
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -158,25 +160,26 @@
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec.build(),
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                         ImmutableSet.of(
                                                 prefix1 + "namespace1", prefix1 + "namespace2"),
                                 prefix2,
                                         ImmutableSet.of(
                                                 prefix2 + "namespace1", prefix2 + "namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                        ImmutableMap.of(
-                                                prefix1 + "typeA", configProto,
-                                                prefix1 + "typeB", configProto),
-                                prefix2,
-                                        ImmutableMap.of(
-                                                prefix2 + "typeA", configProto,
-                                                prefix2 + "typeB", configProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                                ImmutableMap.of(
+                                                        prefix1 + "typeA", configProto,
+                                                        prefix1 + "typeB", configProto),
+                                        prefix2,
+                                                ImmutableMap.of(
+                                                        prefix2 + "typeA", configProto,
+                                                        prefix2 + "typeB", configProto))),
                         mLocalStorageIcingOptionsConfig);
 
         // Convert SearchSpec to proto.
@@ -234,31 +237,32 @@
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec.build(),
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                 ImmutableSet.of(prefix1 + "namespace1", prefix1 + "namespace2"),
                                 prefix2,
                                 ImmutableSet.of(prefix2 + "namespace1", prefix2 + "namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                ImmutableMap.of(
-                                        prefix1 + "typeA", configProto,
-                                        prefix1 + "typeB", configProto),
-                                prefix2,
-                                ImmutableMap.of(
-                                        prefix2 + "typeA", configProto,
-                                        prefix2 + "typeB", configProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                        ImmutableMap.of(
+                                                prefix1 + "typeA", configProto,
+                                                prefix1 + "typeB", configProto),
+                                        prefix2,
+                                        ImmutableMap.of(
+                                                prefix2 + "typeA", configProto,
+                                                prefix2 + "typeB", configProto))),
                         mLocalStorageIcingOptionsConfig);
 
         VisibilityStore visibilityStore = new VisibilityStore(mAppSearchImpl);
         converter.removeInaccessibleSchemaFilter(
-                new CallerAccess(/*callingPackageName=*/ "package"),
+                new CallerAccess(/* callingPackageName= */ "package"),
                 visibilityStore,
                 AppSearchTestUtils.createMockVisibilityChecker(
-                        /*visiblePrefixedSchemas=*/ ImmutableSet.of(
+                        /* visiblePrefixedSchemas= */ ImmutableSet.of(
                                 prefix1 + "typeA",
                                 prefix1 + "typeB",
                                 prefix2 + "typeA",
@@ -300,16 +304,18 @@
 
         ScoringSpecProto scoringSpecProto =
                 new SearchSpecToProtoConverter(
-                                /*queryExpression=*/ "",
+                                /* queryExpression= */ "",
                                 searchSpec,
-                                /*prefixes=*/ ImmutableSet.of(prefix),
-                                /*namespaceMap=*/ ImmutableMap.of(
+                                /* prefixes= */ ImmutableSet.of(prefix),
+                                /* namespaceMap= */ ImmutableMap.of(
                                         prefix, ImmutableSet.of(prefix + namespace)),
-                                /*schemaMap=*/ ImmutableMap.of(
-                                        prefix,
-                                        ImmutableMap.of(
-                                                prefix + schemaType,
-                                                SchemaTypeConfigProto.getDefaultInstance())),
+                                new SchemaCache(
+                                        /* schemaMap= */ ImmutableMap.of(
+                                                prefix,
+                                                ImmutableMap.of(
+                                                        prefix + schemaType,
+                                                        SchemaTypeConfigProto
+                                                                .getDefaultInstance()))),
                                 mLocalStorageIcingOptionsConfig)
                         .toScoringSpecProto();
         TypePropertyWeights typePropertyWeights =
@@ -340,11 +346,11 @@
 
         ScoringSpecProto scoringSpecProto =
                 new SearchSpecToProtoConverter(
-                                /*queryExpression=*/ "query",
+                                /* queryExpression= */ "query",
                                 searchSpec,
-                                /*prefixes=*/ ImmutableSet.of(),
-                                /*namespaceMap=*/ ImmutableMap.of(),
-                                /*schemaMap=*/ ImmutableMap.of(),
+                                /* prefixes= */ ImmutableSet.of(),
+                                /* namespaceMap= */ ImmutableMap.of(),
+                                new SchemaCache(),
                                 mLocalStorageIcingOptionsConfig)
                         .toScoringSpecProto();
 
@@ -368,15 +374,14 @@
 
         SearchSpecToProtoConverter convert =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto =
-                convert.toResultSpecProto(
-                        /*namespaceMap=*/ ImmutableMap.of(), /*schemaMap=*/ ImmutableMap.of());
+                convert.toResultSpecProto(/* namespaceMap= */ ImmutableMap.of(), new SchemaCache());
 
         assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
         assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -412,16 +417,16 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto =
                 converter.toResultSpecProto(
-                        /*namespaceMap=*/ ImmutableMap.of(), /*schemaMap=*/ ImmutableMap.of());
+                        /* namespaceMap= */ ImmutableMap.of(), new SchemaCache());
 
         assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
         assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
@@ -466,14 +471,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(personPrefix, actionPrefix),
+                        /* prefixes= */ ImmutableSet.of(personPrefix, actionPrefix),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
 
-        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+        ResultSpecProto resultSpecProto =
+                converter.toResultSpecProto(namespaceMap, new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(1);
         assertThat(resultSpecProto.getResultGroupings(0).getEntryGroupings(0).getNamespace())
@@ -521,14 +527,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(personPrefix, actionPrefix),
+                        /* prefixes= */ ImmutableSet.of(personPrefix, actionPrefix),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
 
-        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+        ResultSpecProto resultSpecProto =
+                converter.toResultSpecProto(namespaceMap, new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
         assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType())
@@ -557,16 +564,16 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(personPrefix, actionPrefix),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(personPrefix, actionPrefix),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto =
                 converter.toResultSpecProto(
-                        /*namespaceMap=*/ ImmutableMap.of(), /*schemaMap=*/ ImmutableMap.of());
+                        /* namespaceMap= */ ImmutableMap.of(), new SchemaCache());
 
         assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
         assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType())
@@ -586,16 +593,16 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
 
         ResultSpecProto resultSpecProto =
                 converter.toResultSpecProto(
-                        /*namespaceMap=*/ ImmutableMap.of(), /*schemaMap=*/ ImmutableMap.of());
+                        /* namespaceMap= */ ImmutableMap.of(), new SchemaCache());
 
         assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
         assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType())
@@ -604,6 +611,58 @@
     }
 
     @Test
+    public void testToResultSpecProto_projection_removeSchemaWithoutParentInFilter() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .addFilterSchemas("Person")
+                        .addProjection("Artist", ImmutableList.of("name"))
+                        .addProjection("Other", ImmutableList.of("email"))
+                        .build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Person").build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Other").build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(
+                        prefix,
+                        ImmutableMap.of(
+                                "package$database/Person", personSchema,
+                                "package$database/Artist", artistSchema,
+                                "package$database/Other", otherSchema));
+        Map<String, Set<String>> namespaceMap =
+                ImmutableMap.of(prefix, ImmutableSet.of("package$database/namespace"));
+
+        SearchSpecToProtoConverter converter =
+                new SearchSpecToProtoConverter(
+                        /* queryExpression= */ "",
+                        searchSpec,
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ namespaceMap,
+                        new SchemaCache(schemaMap),
+                        mLocalStorageIcingOptionsConfig);
+
+        ResultSpecProto resultSpecProto =
+                converter.toResultSpecProto(namespaceMap, new SchemaCache(schemaMap));
+
+        // The "name" property specified in Artist's projection should remain in the result,
+        // since even though Artist doesn't exist in the original schema filters directly, we have
+        // specified its parent, Person, in the schema filters.
+        // The "email" property specified in Other's projection should be dropped as usual.
+        assertThat(resultSpecProto.getTypePropertyMasksCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getTypePropertyMasks(0).getSchemaType())
+                .isEqualTo("package$database/Artist");
+        assertThat(resultSpecProto.getTypePropertyMasks(0).getPathsCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getTypePropertyMasks(0).getPaths(0)).isEqualTo("name");
+    }
+
+    @Test
     public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
@@ -639,11 +698,11 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(personPrefix, actionPrefix),
+                        /* prefixes= */ ImmutableSet.of(personPrefix, actionPrefix),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -674,11 +733,11 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(personPrefix, actionPrefix),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(personPrefix, actionPrefix),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -690,6 +749,57 @@
     }
 
     @Test
+    public void testToSearchSpecProto_propertyFilter_removeSchemaWithoutParentInFilter() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder()
+                        .addFilterSchemas("Person")
+                        .addFilterProperties("Artist", ImmutableList.of("name"))
+                        .addFilterProperties("Other", ImmutableList.of("email"))
+                        .build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Person").build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Other").build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(
+                        prefix,
+                        ImmutableMap.of(
+                                "package$database/Person", personSchema,
+                                "package$database/Artist", artistSchema,
+                                "package$database/Other", otherSchema));
+        Map<String, Set<String>> namespaceMap =
+                ImmutableMap.of(prefix, ImmutableSet.of("package$database/namespace"));
+
+        SearchSpecToProtoConverter converter =
+                new SearchSpecToProtoConverter(
+                        /* queryExpression= */ "",
+                        searchSpec,
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ namespaceMap,
+                        new SchemaCache(schemaMap),
+                        mLocalStorageIcingOptionsConfig);
+
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+
+        // The "name" property specified in Artist's property filters should remain in the result,
+        // since even though Artist doesn't exist in the original schema filters directly, we have
+        // specified its parent, Person, in the schema filters.
+        // The "email" property specified in Other's property filters should be dropped as usual.
+        assertThat(searchSpecProto.getTypePropertyFiltersCount()).isEqualTo(1);
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getSchemaType())
+                .isEqualTo("package$database/Artist");
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getPathsCount()).isEqualTo(1);
+        assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
+    }
+
+    @Test
     public void testToResultSpecProto_weight_withJoinSpec_packageFilter() throws Exception {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
@@ -726,11 +836,11 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(personPrefix, actionPrefix),
+                        /* prefixes= */ ImmutableSet.of(personPrefix, actionPrefix),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
 
         ScoringSpecProto scoringSpecProto = converter.toScoringSpecProto();
@@ -768,22 +878,22 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto =
                 converter.toResultSpecProto(
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                         ImmutableSet.of(
                                                 prefix1 + "namespaceA", prefix1 + "namespaceB"),
                                 prefix2,
                                         ImmutableSet.of(
                                                 prefix2 + "namespaceA", prefix2 + "namespaceB")),
-                        /*schemaMap=*/ ImmutableMap.of());
+                        new SchemaCache());
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
         // First grouping should have same package name.
@@ -819,14 +929,14 @@
                         prefix2, ImmutableSet.of(prefix2 + "namespaceA", prefix2 + "namespaceB"));
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto =
-                converter.toResultSpecProto(namespaceMap, /*schemaMap=*/ ImmutableMap.of());
+                converter.toResultSpecProto(namespaceMap, new SchemaCache());
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
         // First grouping should have same namespace.
@@ -866,14 +976,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        schemaMap,
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto =
-                converter.toResultSpecProto(/*namespaceMap=*/ ImmutableMap.of(), schemaMap);
+                converter.toResultSpecProto(
+                        /* namespaceMap= */ ImmutableMap.of(), new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
         // First grouping should have the same schema type.
@@ -901,20 +1012,20 @@
         String prefix1 = PrefixUtil.createPrefix("package1", "database");
         String prefix2 = PrefixUtil.createPrefix("package2", "database");
         Map<String, Set<String>> namespaceMap =
-                /*namespaceMap=*/ ImmutableMap.of(
+                /* namespaceMap= */ ImmutableMap.of(
                         prefix1, ImmutableSet.of(prefix1 + "namespaceA", prefix1 + "namespaceB"),
                         prefix2, ImmutableSet.of(prefix2 + "namespaceA", prefix2 + "namespaceB"));
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto =
-                converter.toResultSpecProto(namespaceMap, /*schemaMap=*/ ImmutableMap.of());
+                converter.toResultSpecProto(namespaceMap, new SchemaCache());
 
         // All namespace should be separated.
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -948,14 +1059,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        schemaMap,
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
         ResultSpecProto resultSpecProto =
-                converter.toResultSpecProto(/*namespaceMap=*/ ImmutableMap.of(), schemaMap);
+                converter.toResultSpecProto(
+                        /* namespaceMap= */ ImmutableMap.of(), new SchemaCache(schemaMap));
 
         // All schema should be separated.
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
@@ -978,7 +1090,7 @@
         String prefix1 = PrefixUtil.createPrefix("package1", "database");
         String prefix2 = PrefixUtil.createPrefix("package2", "database");
         Map<String, Set<String>> namespaceMap =
-                /*namespaceMap=*/ ImmutableMap.of(
+                /* namespaceMap= */ ImmutableMap.of(
                         prefix1, ImmutableSet.of(prefix1 + "namespaceA", prefix1 + "namespaceB"),
                         prefix2, ImmutableSet.of(prefix2 + "namespaceA", prefix2 + "namespaceB"));
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
@@ -995,13 +1107,14 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
-        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+        ResultSpecProto resultSpecProto =
+                converter.toResultSpecProto(namespaceMap, new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
         ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
@@ -1066,7 +1179,7 @@
         String prefix1 = PrefixUtil.createPrefix("package1", "database");
         String prefix2 = PrefixUtil.createPrefix("package2", "database");
         Map<String, Set<String>> namespaceMap =
-                /*namespaceMap=*/ ImmutableMap.of(
+                /* namespaceMap= */ ImmutableMap.of(
                         prefix1, ImmutableSet.of(prefix1 + "namespaceA", prefix1 + "namespaceB"),
                         prefix2, ImmutableSet.of(prefix2 + "namespaceA", prefix2 + "namespaceB"));
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
@@ -1083,13 +1196,14 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "query",
+                        /* queryExpression= */ "query",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
-        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap, schemaMap);
+        ResultSpecProto resultSpecProto =
+                converter.toResultSpecProto(namespaceMap, new SchemaCache(schemaMap));
 
         assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(8);
         ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
@@ -1176,11 +1290,11 @@
                                         "package$database2/namespace4"));
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1200,10 +1314,10 @@
         // Only search for prefix1
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                         ImmutableSet.of(
                                                 "package$database1/namespace1",
@@ -1212,7 +1326,7 @@
                                         ImmutableSet.of(
                                                 "package$database2/namespace3",
                                                 "package$database2/namespace4")),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1230,15 +1344,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                 ImmutableSet.of(
                                         "package$database1/namespace1",
                                         "package$database1/namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching namespace filter is not empty, the target namespace filter will be the
@@ -1256,15 +1370,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                 ImmutableSet.of(
                                         "package$database1/namespace1",
                                         "package$database1/namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching namespace filter is not empty, the target namespace filter will be the
@@ -1282,20 +1396,25 @@
                 SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                        ImmutableMap.of(
-                                                "package$database1/typeA", schemaTypeConfigProto,
-                                                "package$database1/typeB", schemaTypeConfigProto),
-                                prefix2,
-                                        ImmutableMap.of(
-                                                "package$database2/typeC", schemaTypeConfigProto,
-                                                "package$database2/typeD", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                                ImmutableMap.of(
+                                                        "package$database1/typeA",
+                                                                schemaTypeConfigProto,
+                                                        "package$database1/typeB",
+                                                                schemaTypeConfigProto),
+                                        prefix2,
+                                                ImmutableMap.of(
+                                                        "package$database2/typeC",
+                                                                schemaTypeConfigProto,
+                                                        "package$database2/typeD",
+                                                                schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // Empty searching filter will get all types for target filter
@@ -1315,20 +1434,25 @@
         // only search in prefix1
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                        ImmutableMap.of(
-                                                "package$database1/typeA", schemaTypeConfigProto,
-                                                "package$database1/typeB", schemaTypeConfigProto),
-                                prefix2,
-                                        ImmutableMap.of(
-                                                "package$database2/typeC", schemaTypeConfigProto,
-                                                "package$database2/typeD", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                                ImmutableMap.of(
+                                                        "package$database1/typeA",
+                                                                schemaTypeConfigProto,
+                                                        "package$database1/typeB",
+                                                                schemaTypeConfigProto),
+                                        prefix2,
+                                                ImmutableMap.of(
+                                                        "package$database2/typeC",
+                                                                schemaTypeConfigProto,
+                                                        "package$database2/typeD",
+                                                                schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // Only search prefix1 will return typeA and B.
@@ -1346,16 +1470,17 @@
                 SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                ImmutableMap.of(
-                                        "package$database1/typeA", schemaTypeConfigProto,
-                                        "package$database1/typeB", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                        ImmutableMap.of(
+                                                "package$database1/typeA", schemaTypeConfigProto,
+                                                "package$database1/typeB", schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If the searching schema filter is not empty, the target schema filter will be the
@@ -1366,6 +1491,97 @@
     }
 
     @Test
+    public void testGetTargetSchemaFilters_polymorphismExpansion() {
+        SearchSpec searchSpec =
+                new SearchSpec.Builder().addFilterSchemas("Person", "nonExist").build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto personSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Person").build();
+        SchemaTypeConfigProto artistSchema =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/Artist")
+                        .addParentTypes("package$database/Person")
+                        .build();
+        SchemaTypeConfigProto otherSchema =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/Other").build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(
+                        prefix,
+                        ImmutableMap.of(
+                                "package$database/Person", personSchema,
+                                "package$database/Artist", artistSchema,
+                                "package$database/Other", otherSchema));
+        SearchSpecToProtoConverter converter =
+                new SearchSpecToProtoConverter(
+                        /* queryExpression= */ "",
+                        searchSpec,
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ ImmutableMap.of(
+                                prefix, ImmutableSet.of("package$database/namespace")),
+                        new SchemaCache(schemaMap),
+                        mLocalStorageIcingOptionsConfig);
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+        // The schema filter of "Person" specified in searchSpec will be expanded to "Artist" via
+        // polymorphism.
+        assertThat(searchSpecProto.getSchemaTypeFiltersList())
+                .containsExactly("package$database/Person", "package$database/Artist");
+    }
+
+    @Test
+    public void testGetTargetSchemaFilters_polymorphismExpansion_multipleLevel() {
+        SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("A", "B").build();
+        String prefix = createPrefix("package", "database");
+        SchemaTypeConfigProto schemaA =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/A").build();
+        SchemaTypeConfigProto schemaB =
+                SchemaTypeConfigProto.newBuilder().setSchemaType("package$database/B").build();
+        SchemaTypeConfigProto schemaC =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/C")
+                        .addParentTypes("package$database/A")
+                        .build();
+        SchemaTypeConfigProto schemaD =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/D")
+                        .addParentTypes("package$database/C")
+                        .build();
+        SchemaTypeConfigProto schemaE =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType("package$database/E")
+                        .addParentTypes("package$database/B")
+                        .addParentTypes("package$database/C")
+                        .build();
+
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap =
+                ImmutableMap.of(
+                        prefix,
+                        ImmutableMap.of(
+                                "package$database/A", schemaA,
+                                "package$database/B", schemaB,
+                                "package$database/C", schemaC,
+                                "package$database/D", schemaD,
+                                "package$database/E", schemaE));
+        SearchSpecToProtoConverter converter =
+                new SearchSpecToProtoConverter(
+                        /* queryExpression= */ "",
+                        searchSpec,
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ ImmutableMap.of(
+                                prefix, ImmutableSet.of("package$database/namespace")),
+                        new SchemaCache(schemaMap),
+                        mLocalStorageIcingOptionsConfig);
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
+        assertThat(searchSpecProto.getSchemaTypeFiltersList())
+                .containsExactly(
+                        "package$database/A",
+                        "package$database/B",
+                        "package$database/C",
+                        "package$database/D",
+                        "package$database/E");
+    }
+
+    @Test
     public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() {
         // Put non-exist searching schema.
         SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("nonExist").build();
@@ -1374,16 +1590,17 @@
                 SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1, ImmutableSet.of("package$database1/namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                ImmutableMap.of(
-                                        "package$database1/typeA", schemaTypeConfigProto,
-                                        "package$database1/typeB", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                        ImmutableMap.of(
+                                                "package$database1/typeA", schemaTypeConfigProto,
+                                                "package$database1/typeB", schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         // If there is no intersection of the schema filters that user want to search over and
@@ -1405,24 +1622,26 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         new SearchSpec.Builder().setJoinSpec(joinSpec).build(),
-                        /*prefixes=*/ ImmutableSet.of(prefix),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix, ImmutableSet.of("package$database/namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix,
-                                ImmutableMap.of(
-                                        "package$database/schema1", schemaTypeConfigProto,
-                                        "package$database/schema2", schemaTypeConfigProto,
-                                        "package$database/schema3", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix,
+                                        ImmutableMap.of(
+                                                "package$database/schema1", schemaTypeConfigProto,
+                                                "package$database/schema2", schemaTypeConfigProto,
+                                                "package$database/schema3",
+                                                        schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
 
         converter.removeInaccessibleSchemaFilter(
-                new CallerAccess(/*callingPackageName=*/ "otherPackageName"),
+                new CallerAccess(/* callingPackageName= */ "otherPackageName"),
                 visibilityStore,
                 AppSearchTestUtils.createMockVisibilityChecker(
-                        /*visiblePrefixedSchemas=*/ ImmutableSet.of(
+                        /* visiblePrefixedSchemas= */ ImmutableSet.of(
                                 prefix + "schema1", prefix + "schema3")));
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -1460,39 +1679,39 @@
 
         SearchSpecToProtoConverter emptySchemaConverter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix),
-                        /*namespaceMap=*/ namespaceMap,
-                        /*schemaMap=*/ ImmutableMap.of(),
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ namespaceMap,
+                        new SchemaCache(),
                         mLocalStorageIcingOptionsConfig);
         assertThat(emptySchemaConverter.hasNothingToSearch()).isTrue();
 
         SearchSpecToProtoConverter emptyNamespaceConverter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        schemaMap,
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
         assertThat(emptyNamespaceConverter.hasNothingToSearch()).isTrue();
 
         SearchSpecToProtoConverter nonEmptyConverter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix),
+                        /* prefixes= */ ImmutableSet.of(prefix),
                         namespaceMap,
-                        schemaMap,
+                        new SchemaCache(schemaMap),
                         mLocalStorageIcingOptionsConfig);
         assertThat(nonEmptyConverter.hasNothingToSearch()).isFalse();
 
         // remove all target schema filter, and the query becomes nothing to search.
         nonEmptyConverter.removeInaccessibleSchemaFilter(
-                new CallerAccess(/*callingPackageName=*/ "otherPackageName"),
-                /*visibilityStore=*/ null,
-                /*visibilityChecker=*/ null);
+                new CallerAccess(/* callingPackageName= */ "otherPackageName"),
+                /* visibilityStore= */ null,
+                /* visibilityChecker= */ null);
         assertThat(nonEmptyConverter.hasNothingToSearch()).isTrue();
         // As the JoinSpec has nothing to search, it should not be part of the SearchSpec
         assertThat(nonEmptyConverter.toSearchSpecProto().hasJoinSpec()).isFalse();
@@ -1515,24 +1734,26 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         new SearchSpec.Builder().setJoinSpec(joinSpec).build(),
-                        /*prefixes=*/ ImmutableSet.of(prefix),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix, ImmutableSet.of("package$database/namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix,
-                                ImmutableMap.of(
-                                        "package$database/schema1", schemaTypeConfigProto,
-                                        "package$database/schema2", schemaTypeConfigProto,
-                                        "package$database/schema3", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix,
+                                        ImmutableMap.of(
+                                                "package$database/schema1", schemaTypeConfigProto,
+                                                "package$database/schema2", schemaTypeConfigProto,
+                                                "package$database/schema3",
+                                                        schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
 
         converter.removeInaccessibleSchemaFilter(
-                new CallerAccess(/*callingPackageName=*/ "otherPackageName"),
+                new CallerAccess(/* callingPackageName= */ "otherPackageName"),
                 visibilityStore,
                 AppSearchTestUtils.createMockVisibilityChecker(
-                        /*visiblePrefixedSchemas=*/ ImmutableSet.of(prefix + "schema3")));
+                        /* visiblePrefixedSchemas= */ ImmutableSet.of(prefix + "schema3")));
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(prefix + "schema3");
@@ -1575,11 +1796,11 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1, prefix2),
+                        /* prefixes= */ ImmutableSet.of(prefix1, prefix2),
                         namespaceMap,
-                        schemaTypeMap,
+                        new SchemaCache(schemaTypeMap),
                         mLocalStorageIcingOptionsConfig);
 
         TypePropertyWeights expectedTypePropertyWeight1 =
@@ -1626,13 +1847,15 @@
 
         SearchSpecToProtoConverter converter =
                 new SearchSpecToProtoConverter(
-                        /*queryExpression=*/ "",
+                        /* queryExpression= */ "",
                         searchSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1, ImmutableSet.of(prefix1 + "namespace1")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1, ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto)),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                        ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto))),
                         mLocalStorageIcingOptionsConfig);
 
         ScoringSpecProto convertedScoringSpecProto = converter.toScoringSpecProto();
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
index e786fac..f16a89c 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
@@ -20,6 +20,7 @@
 
 import android.app.appsearch.SearchSuggestionSpec;
 
+import com.android.server.appsearch.external.localstorage.SchemaCache;
 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
 import com.android.server.appsearch.icing.proto.NamespaceDocumentUriGroup;
 import com.android.server.appsearch.icing.proto.SchemaTypeConfigProto;
@@ -37,7 +38,7 @@
     @Test
     public void testToProto() throws Exception {
         SearchSuggestionSpec searchSuggestionSpec =
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 123)
+                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 123)
                         .setRankingStrategy(
                                 SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
                         .addFilterNamespaces("namespace1", "namespace2")
@@ -49,17 +50,18 @@
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
         SearchSuggestionSpecToProtoConverter converter =
                 new SearchSuggestionSpecToProtoConverter(
-                        /*queryExpression=*/ "prefix",
+                        /* queryExpression= */ "prefix",
                         searchSuggestionSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(
                                 prefix1,
                                 ImmutableSet.of(prefix1 + "namespace1", prefix1 + "namespace2")),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                ImmutableMap.of(
-                                        prefix1 + "typeA", configProto,
-                                        prefix1 + "typeB", configProto)));
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                        ImmutableMap.of(
+                                                prefix1 + "typeA", configProto,
+                                                prefix1 + "typeB", configProto))));
 
         SuggestionSpecProto proto = converter.toSearchSuggestionSpecProto();
 
@@ -81,7 +83,7 @@
     @Test
     public void testToProto_propertyFilters() throws Exception {
         SearchSuggestionSpec searchSuggestionSpec =
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/ 123)
+                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 123)
                         .addFilterProperties("typeA", ImmutableList.of("property1", "property2"))
                         .build();
 
@@ -89,15 +91,16 @@
         SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
         SearchSuggestionSpecToProtoConverter converter =
                 new SearchSuggestionSpecToProtoConverter(
-                        /*queryExpression=*/ "prefix",
+                        /* queryExpression= */ "prefix",
                         searchSuggestionSpec,
-                        /*prefixes=*/ ImmutableSet.of(prefix1),
-                        /*namespaceMap=*/ ImmutableMap.of(),
-                        /*schemaMap=*/ ImmutableMap.of(
-                                prefix1,
-                                ImmutableMap.of(
-                                        prefix1 + "typeA", configProto,
-                                        prefix1 + "typeB", configProto)));
+                        /* prefixes= */ ImmutableSet.of(prefix1),
+                        /* namespaceMap= */ ImmutableMap.of(),
+                        new SchemaCache(
+                                /* schemaMap= */ ImmutableMap.of(
+                                        prefix1,
+                                        ImmutableMap.of(
+                                                prefix1 + "typeA", configProto,
+                                                prefix1 + "typeB", configProto))));
 
         SuggestionSpecProto proto = converter.toSearchSuggestionSpecProto();
         assertThat(proto.getTypePropertyFiltersList())
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java
index 4d5091f..8844dcb 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/converter/SnippetTest.java
@@ -24,6 +24,7 @@
 
 import com.android.server.appsearch.external.localstorage.AppSearchConfigImpl;
 import com.android.server.appsearch.external.localstorage.LocalStorageIcingOptionsConfig;
+import com.android.server.appsearch.external.localstorage.SchemaCache;
 import com.android.server.appsearch.external.localstorage.UnlimitedLimitConfig;
 import com.android.server.appsearch.external.localstorage.util.PrefixUtil;
 import com.android.server.appsearch.icing.proto.DocumentProto;
@@ -103,7 +104,7 @@
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(
                         searchResultProto,
-                        SCHEMA_MAP,
+                        new SchemaCache(SCHEMA_MAP),
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
@@ -112,13 +113,13 @@
         assertThat(match.getFullText()).isEqualTo(propertyValueString);
         assertThat(match.getExactMatch()).isEqualTo(exactMatch);
         assertThat(match.getExactMatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 29, /*upper=*/ 32));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 29, /* upper= */ 32));
         assertThat(match.getSubmatch()).isEqualTo("foo");
         assertThat(match.getSubmatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 29, /*upper=*/ 32));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 29, /* upper= */ 32));
         assertThat(match.getFullText()).isEqualTo(propertyValueString);
         assertThat(match.getSnippetRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 26, /*upper=*/ 32));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 26, /* upper= */ 32));
         assertThat(match.getSnippet()).isEqualTo(window);
     }
 
@@ -152,7 +153,7 @@
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(
                         searchResultProto,
-                        SCHEMA_MAP,
+                        new SchemaCache(SCHEMA_MAP),
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
@@ -221,7 +222,7 @@
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(
                         searchResultProto,
-                        SCHEMA_MAP,
+                        new SchemaCache(SCHEMA_MAP),
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
@@ -229,26 +230,26 @@
         assertThat(match1.getPropertyPath()).isEqualTo("senderName");
         assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
         assertThat(match1.getExactMatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 4));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 4));
         assertThat(match1.getExactMatch()).isEqualTo("Test");
         assertThat(match1.getSubmatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 4));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 4));
         assertThat(match1.getSubmatch()).isEqualTo("Test");
         assertThat(match1.getSnippetRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 9));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 9));
         assertThat(match1.getSnippet()).isEqualTo("Test Name");
 
         SearchResult.MatchInfo match2 = searchResultPage.getResults().get(0).getMatchInfos().get(1);
         assertThat(match2.getPropertyPath()).isEqualTo("senderEmail");
         assertThat(match2.getFullText()).isEqualTo("[email protected]");
         assertThat(match2.getExactMatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 20));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 20));
         assertThat(match2.getExactMatch()).isEqualTo("[email protected]");
         assertThat(match2.getSubmatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 4));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 4));
         assertThat(match2.getSubmatch()).isEqualTo("Test");
         assertThat(match2.getSnippetRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 20));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 20));
         assertThat(match2.getSnippet()).isEqualTo("[email protected]");
     }
 
@@ -325,7 +326,7 @@
         SearchResultPage searchResultPage =
                 SearchResultToProtoConverter.toSearchResultPage(
                         searchResultProto,
-                        SCHEMA_MAP,
+                        new SchemaCache(SCHEMA_MAP),
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()));
         assertThat(searchResultPage.getResults()).hasSize(1);
@@ -334,13 +335,13 @@
         assertThat(match1.getPropertyPathObject()).isEqualTo(new PropertyPath("sender.name"));
         assertThat(match1.getFullText()).isEqualTo("Test Name Jr.");
         assertThat(match1.getExactMatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 4));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 4));
         assertThat(match1.getExactMatch()).isEqualTo("Test");
         assertThat(match1.getSubmatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 4));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 4));
         assertThat(match1.getSubmatch()).isEqualTo("Test");
         assertThat(match1.getSnippetRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 9));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 9));
         assertThat(match1.getSnippet()).isEqualTo("Test Name");
 
         SearchResult.MatchInfo match2 = searchResultPage.getResults().get(0).getMatchInfos().get(1);
@@ -348,13 +349,13 @@
         assertThat(match2.getPropertyPathObject()).isEqualTo(new PropertyPath("sender.email[1]"));
         assertThat(match2.getFullText()).isEqualTo("[email protected]");
         assertThat(match2.getExactMatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 21));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 21));
         assertThat(match2.getExactMatch()).isEqualTo("[email protected]");
         assertThat(match2.getSubmatchRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 4));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 4));
         assertThat(match2.getSubmatch()).isEqualTo("Test");
         assertThat(match2.getSnippetRange())
-                .isEqualTo(new SearchResult.MatchRange(/*lower=*/ 0, /*upper=*/ 21));
+                .isEqualTo(new SearchResult.MatchRange(/* lower= */ 0, /* upper= */ 21));
         assertThat(match2.getSnippet()).isEqualTo("[email protected]");
     }
 }
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/ClickStatsTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/ClickStatsTest.java
new file mode 100644
index 0000000..85c595a
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/ClickStatsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ClickStatsTest {
+    @Test
+    public void testBuilder() {
+        long timestampMillis = 1L;
+        long timeStayOnResultMillis = 2L;
+        int resultRankInBlock = 3;
+        int resultRankGlobal = 4;
+        boolean isGoodClick = false;
+
+        final ClickStats clickStats =
+                new ClickStats.Builder()
+                        .setTimestampMillis(timestampMillis)
+                        .setTimeStayOnResultMillis(timeStayOnResultMillis)
+                        .setResultRankInBlock(resultRankInBlock)
+                        .setResultRankGlobal(resultRankGlobal)
+                        .setIsGoodClick(isGoodClick)
+                        .build();
+
+        assertThat(clickStats.getTimestampMillis()).isEqualTo(timestampMillis);
+        assertThat(clickStats.getTimeStayOnResultMillis()).isEqualTo(timeStayOnResultMillis);
+        assertThat(clickStats.getResultRankInBlock()).isEqualTo(resultRankInBlock);
+        assertThat(clickStats.getResultRankGlobal()).isEqualTo(resultRankGlobal);
+        assertThat(clickStats.isGoodClick()).isEqualTo(isGoodClick);
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/SearchIntentStatsTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/SearchIntentStatsTest.java
new file mode 100644
index 0000000..b4fd893
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/SearchIntentStatsTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SearchIntentStatsTest {
+    static final String TEST_PACKAGE_NAME = "package.test";
+    static final String TEST_DATA_BASE = "testDataBase";
+
+    @Test
+    public void testBuilder() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1L;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        // Clicks associated with the search intent.
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        final SearchIntentStats searchIntentStats =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(clickStats0, clickStats1)
+                        .build();
+
+        assertThat(searchIntentStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats.getClicksStats()).containsExactly(clickStats0, clickStats1);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsAreCopied() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1L;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        // Clicks associated with the search intent.
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(clickStats0, clickStats1)
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(searchIntentStats0).build();
+
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats1.getClicksStats()).containsExactly(clickStats0, clickStats1);
+    }
+
+    @Test
+    public void testBuilderCopy_copiedFieldsCanBeUpdated() {
+        // Clicks associated with the search intent.
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+                        .addClicksStats(clickStats0, clickStats1)
+                        .build();
+
+        // Build another SearchIntentStats based on the previous one, with fields changed.
+        final ClickStats clickStats2 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(12L)
+                        .setTimeStayOnResultMillis(22L)
+                        .setResultRankInBlock(32)
+                        .setResultRankGlobal(42)
+                        .setIsGoodClick(true)
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(searchIntentStats0)
+                        .setDatabase("database2")
+                        .setPrevQuery("query3")
+                        .setCurrQuery("query4")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(clickStats2)
+                        .build();
+
+        // Check that searchIntentStats0 wasn't altered.
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats0.getPrevQuery()).isEqualTo("query1");
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("query2");
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1L);
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(2);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+        // Check that searchIntentStats1 has the new values.
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo("database2");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("query3");
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("query4");
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2L);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(4);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats())
+                .containsExactly(clickStats0, clickStats1, clickStats2);
+    }
+
+    @Test
+    public void testBuilder_addClicksStats_byCollection() {
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+        Set<ClickStats> clicksStats = ImmutableSet.of(clickStats0, clickStats1);
+
+        final SearchIntentStats searchIntentStats =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addClicksStats(clicksStats)
+                        .build();
+
+        assertThat(searchIntentStats.getClicksStats()).containsExactlyElementsIn(clicksStats);
+    }
+
+    @Test
+    public void testBuilder_builderReuse() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        SearchIntentStats.Builder builder =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(clickStats0, clickStats1);
+
+        final SearchIntentStats searchIntentStats0 = builder.build();
+
+        final ClickStats clickStats2 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(12L)
+                        .setTimeStayOnResultMillis(22L)
+                        .setResultRankInBlock(32)
+                        .setResultRankGlobal(42)
+                        .setIsGoodClick(true)
+                        .build();
+        builder.addClicksStats(clickStats2);
+
+        final SearchIntentStats searchIntentStats1 = builder.build();
+
+        // Check that searchIntentStats0 wasn't altered.
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+        // Check that searchIntentStats1 has the new values.
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats1.getClicksStats())
+                .containsExactly(clickStats0, clickStats1, clickStats2);
+    }
+
+    @Test
+    public void testBuilder_builderReuse_byCollection() {
+        String prevQuery = "prev";
+        String currQuery = "curr";
+        long searchIntentTimestampMillis = 1;
+        int numResultsFetched = 2;
+        int queryCorrectionType = SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT;
+
+        final ClickStats clickStats0 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(10L)
+                        .setTimeStayOnResultMillis(20L)
+                        .setResultRankInBlock(30)
+                        .setResultRankGlobal(40)
+                        .setIsGoodClick(false)
+                        .build();
+        final ClickStats clickStats1 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(11L)
+                        .setTimeStayOnResultMillis(21L)
+                        .setResultRankInBlock(31)
+                        .setResultRankGlobal(41)
+                        .setIsGoodClick(true)
+                        .build();
+
+        SearchIntentStats.Builder builder =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery(prevQuery)
+                        .setCurrQuery(currQuery)
+                        .setTimestampMillis(searchIntentTimestampMillis)
+                        .setNumResultsFetched(numResultsFetched)
+                        .setQueryCorrectionType(queryCorrectionType)
+                        .addClicksStats(ImmutableSet.of(clickStats0, clickStats1));
+
+        final SearchIntentStats searchIntentStats0 = builder.build();
+
+        final ClickStats clickStats2 =
+                new ClickStats.Builder()
+                        .setTimestampMillis(12L)
+                        .setTimeStayOnResultMillis(22L)
+                        .setResultRankInBlock(32)
+                        .setResultRankGlobal(42)
+                        .setIsGoodClick(true)
+                        .build();
+        builder.addClicksStats(ImmutableSet.of(clickStats2));
+
+        final SearchIntentStats searchIntentStats1 = builder.build();
+
+        // Check that searchIntentStats0 wasn't altered.
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats0.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats0.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats0.getClicksStats()).containsExactly(clickStats0, clickStats1);
+
+        // Check that searchIntentStats1 has the new values.
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo(prevQuery);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo(currQuery);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(searchIntentTimestampMillis);
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(numResultsFetched);
+        assertThat(searchIntentStats1.getQueryCorrectionType()).isEqualTo(queryCorrectionType);
+        assertThat(searchIntentStats1.getClicksStats())
+                .containsExactly(clickStats0, clickStats1, clickStats2);
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/SearchSessionStatsTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/SearchSessionStatsTest.java
new file mode 100644
index 0000000..c62ce0d
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/stats/SearchSessionStatsTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SearchSessionStatsTest {
+    static final String TEST_PACKAGE_NAME = "package.test";
+    static final String TEST_DATA_BASE = "testDataBase";
+
+    @Test
+    public void testBuilder() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentStats0, searchIntentStats1)
+                        .build();
+
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1);
+    }
+
+    @Test
+    public void testBuilder_addSearchIntentsStats_byCollection() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        Set<SearchIntentStats> searchIntentsStats =
+                ImmutableSet.of(searchIntentStats0, searchIntentStats1);
+
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentsStats)
+                        .build();
+
+        assertThat(searchSessionStats.getSearchIntentsStats())
+                .containsExactlyElementsIn(searchIntentsStats);
+    }
+
+    @Test
+    public void testBuilder_builderReuse() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        SearchSessionStats.Builder builder =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentStats0, searchIntentStats1);
+
+        final SearchSessionStats searchSessionStats0 = builder.build();
+
+        final SearchIntentStats searchIntentStats2 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query2")
+                        .setCurrQuery("query3")
+                        .setTimestampMillis(3L)
+                        .setNumResultsFetched(6)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(13L)
+                                        .setTimeStayOnResultMillis(23L)
+                                        .setResultRankInBlock(33)
+                                        .setResultRankGlobal(43)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        builder.addSearchIntentsStats(searchIntentStats2);
+
+        final SearchSessionStats searchSessionStats1 = builder.build();
+
+        // Check that searchSessionStats0 wasn't altered.
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1);
+
+        // Check that searchSessionStats1 has the new values.
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1, searchIntentStats2);
+    }
+
+    @Test
+    public void testBuilder_builderReuse_byCollection() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        SearchSessionStats.Builder builder =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(
+                                ImmutableSet.of(searchIntentStats0, searchIntentStats1));
+
+        final SearchSessionStats searchSessionStats0 = builder.build();
+
+        final SearchIntentStats searchIntentStats2 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query2")
+                        .setCurrQuery("query3")
+                        .setTimestampMillis(3L)
+                        .setNumResultsFetched(6)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(13L)
+                                        .setTimeStayOnResultMillis(23L)
+                                        .setResultRankInBlock(33)
+                                        .setResultRankGlobal(43)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        builder.addSearchIntentsStats(ImmutableSet.of(searchIntentStats2));
+
+        final SearchSessionStats searchSessionStats1 = builder.build();
+
+        // Check that searchSessionStats0 wasn't altered.
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1);
+
+        // Check that searchSessionStats1 has the new values.
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATA_BASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats())
+                .containsExactly(searchIntentStats0, searchIntentStats1, searchIntentStats2);
+    }
+
+    @Test
+    public void testGetEndSessionSearchIntentStats() {
+        final SearchIntentStats searchIntentStats0 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("")
+                        .setCurrQuery("query1")
+                        .setTimestampMillis(1L)
+                        .setNumResultsFetched(2)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(10L)
+                                        .setTimeStayOnResultMillis(20L)
+                                        .setResultRankInBlock(30)
+                                        .setResultRankGlobal(40)
+                                        .setIsGoodClick(false)
+                                        .build(),
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(11L)
+                                        .setTimeStayOnResultMillis(21L)
+                                        .setResultRankInBlock(31)
+                                        .setResultRankGlobal(41)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+        final SearchIntentStats searchIntentStats1 =
+                new SearchIntentStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .setPrevQuery("query1")
+                        .setCurrQuery("query2")
+                        .setTimestampMillis(2L)
+                        .setNumResultsFetched(4)
+                        .setQueryCorrectionType(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT)
+                        .addClicksStats(
+                                new ClickStats.Builder()
+                                        .setTimestampMillis(12L)
+                                        .setTimeStayOnResultMillis(22L)
+                                        .setResultRankInBlock(32)
+                                        .setResultRankGlobal(42)
+                                        .setIsGoodClick(true)
+                                        .build())
+                        .build();
+
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .addSearchIntentsStats(searchIntentStats0, searchIntentStats1)
+                        .build();
+
+        SearchIntentStats endSessionSearchIntentStats =
+                searchSessionStats.getEndSessionSearchIntentStats();
+        // End session SearchIntentStats should be identical to the last added SearchIntentStats,
+        // except the previous query is null and query correction type is
+        // QUERY_CORRECTION_TYPE_END_SESSION.
+        assertThat(endSessionSearchIntentStats).isNotNull();
+        assertThat(endSessionSearchIntentStats.getPackageName())
+                .isEqualTo(searchIntentStats1.getPackageName());
+        assertThat(endSessionSearchIntentStats.getDatabase())
+                .isEqualTo(searchIntentStats1.getDatabase());
+        assertThat(endSessionSearchIntentStats.getCurrQuery())
+                .isEqualTo(searchIntentStats1.getCurrQuery());
+        assertThat(endSessionSearchIntentStats.getTimestampMillis())
+                .isEqualTo(searchIntentStats1.getTimestampMillis());
+        assertThat(endSessionSearchIntentStats.getNumResultsFetched())
+                .isEqualTo(searchIntentStats1.getNumResultsFetched());
+        assertThat(endSessionSearchIntentStats.getClicksStats())
+                .containsExactlyElementsIn(searchIntentStats1.getClicksStats());
+
+        assertThat(endSessionSearchIntentStats.getPrevQuery()).isNull();
+        assertThat(endSessionSearchIntentStats.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_END_SESSION);
+    }
+
+    @Test
+    public void testGetEndSessionSearchIntentStats_emptySearchIntentsShouldReturnNull() {
+        // Create a SearchSessionStats without search intents.
+        final SearchSessionStats searchSessionStats =
+                new SearchSessionStats.Builder(TEST_PACKAGE_NAME)
+                        .setDatabase(TEST_DATA_BASE)
+                        .build();
+
+        assertThat(searchSessionStats.getEndSessionSearchIntentStats()).isNull();
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/ClickActionGenericDocumentTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/ClickActionGenericDocumentTest.java
new file mode 100644
index 0000000..7d4e028
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/ClickActionGenericDocumentTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.usagereporting.ActionConstants;
+
+import org.junit.Test;
+
+public class ClickActionGenericDocumentTest {
+    @Test
+    public void testBuild() {
+        ClickActionGenericDocument clickActionGenericDocument =
+                new ClickActionGenericDocument.Builder("namespace", "click", "builtin:ClickAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("body")
+                        .setResultRankInBlock(12)
+                        .setResultRankGlobal(34)
+                        .setTimeStayOnResultMillis(2000)
+                        .build();
+
+        assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+        assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+        assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(clickActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+        assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+        assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+    }
+
+    @Test
+    public void testBuild_fromGenericDocument() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyLong("resultRankInBlock", 12)
+                        .setPropertyLong("resultRankGlobal", 34)
+                        .setPropertyLong("timeStayOnResultMillis", 2000)
+                        .build();
+        ClickActionGenericDocument clickActionGenericDocument =
+                new ClickActionGenericDocument.Builder(document).build();
+
+        assertThat(clickActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(clickActionGenericDocument.getId()).isEqualTo("click");
+        assertThat(clickActionGenericDocument.getSchemaType()).isEqualTo("builtin:ClickAction");
+        assertThat(clickActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(clickActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(clickActionGenericDocument.getResultRankInBlock()).isEqualTo(12);
+        assertThat(clickActionGenericDocument.getResultRankGlobal()).isEqualTo(34);
+        assertThat(clickActionGenericDocument.getTimeStayOnResultMillis()).isEqualTo(2000);
+    }
+
+    @Test
+    public void testBuild_invalidActionTypeThrowsException() {
+        GenericDocument documentWithoutActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction").build();
+        IllegalArgumentException e1 =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () -> new ClickActionGenericDocument.Builder(documentWithoutActionType));
+        assertThat(e1.getMessage()).isEqualTo("Invalid action type for ClickActionGenericDocument");
+
+        GenericDocument documentWithUnknownActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:ClickAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)
+                        .build();
+        IllegalArgumentException e2 =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                new ClickActionGenericDocument.Builder(
+                                        documentWithUnknownActionType));
+        assertThat(e2.getMessage()).isEqualTo("Invalid action type for ClickActionGenericDocument");
+
+        GenericDocument documentWithIncorrectActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)
+                        .build();
+        IllegalArgumentException e3 =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                new ClickActionGenericDocument.Builder(
+                                        documentWithIncorrectActionType));
+        assertThat(e3.getMessage()).isEqualTo("Invalid action type for ClickActionGenericDocument");
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/SearchActionGenericDocumentTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/SearchActionGenericDocumentTest.java
new file mode 100644
index 0000000..e4449a4
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/SearchActionGenericDocumentTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.usagereporting.ActionConstants;
+
+import org.junit.Test;
+
+public class SearchActionGenericDocumentTest {
+    @Test
+    public void testBuild() {
+        SearchActionGenericDocument searchActionGenericDocument =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("body")
+                        .setFetchedResultCount(123)
+                        .build();
+
+        assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+        assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+        assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(searchActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+    }
+
+    @Test
+    public void testBuild_fromGenericDocument() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_SEARCH)
+                        .setPropertyString("query", "body")
+                        .setPropertyLong("fetchedResultCount", 123)
+                        .build();
+        SearchActionGenericDocument searchActionGenericDocument =
+                new SearchActionGenericDocument(document);
+
+        assertThat(searchActionGenericDocument.getNamespace()).isEqualTo("namespace");
+        assertThat(searchActionGenericDocument.getId()).isEqualTo("search");
+        assertThat(searchActionGenericDocument.getSchemaType()).isEqualTo("builtin:SearchAction");
+        assertThat(searchActionGenericDocument.getCreationTimestampMillis()).isEqualTo(1000);
+        assertThat(searchActionGenericDocument.getActionType())
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchActionGenericDocument.getQuery()).isEqualTo("body");
+        assertThat(searchActionGenericDocument.getFetchedResultCount()).isEqualTo(123);
+    }
+
+    @Test
+    public void testBuild_invalidActionTypeThrowsException() {
+        GenericDocument documentWithoutActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .build();
+        IllegalArgumentException e1 =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () -> new SearchActionGenericDocument.Builder(documentWithoutActionType));
+        assertThat(e1.getMessage())
+                .isEqualTo("Invalid action type for SearchActionGenericDocument");
+
+        GenericDocument documentWithUnknownActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_UNKNOWN)
+                        .build();
+        IllegalArgumentException e2 =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                new SearchActionGenericDocument.Builder(
+                                        documentWithUnknownActionType));
+        assertThat(e2.getMessage())
+                .isEqualTo("Invalid action type for SearchActionGenericDocument");
+
+        GenericDocument documentWithIncorrectActionType =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setPropertyLong("actionType", ActionConstants.ACTION_TYPE_CLICK)
+                        .build();
+        IllegalArgumentException e3 =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                new SearchActionGenericDocument.Builder(
+                                        documentWithIncorrectActionType));
+        assertThat(e3.getMessage())
+                .isEqualTo("Invalid action type for SearchActionGenericDocument");
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/SearchSessionStatsExtractorTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/SearchSessionStatsExtractorTest.java
new file mode 100644
index 0000000..d7c78af
--- /dev/null
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/usagereporting/SearchSessionStatsExtractorTest.java
@@ -0,0 +1,1219 @@
+/*
+ * Copyright 2024 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 com.android.server.appsearch.external.localstorage.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.appsearch.GenericDocument;
+
+import com.android.server.appsearch.external.localstorage.stats.SearchIntentStats;
+import com.android.server.appsearch.external.localstorage.stats.SearchSessionStats;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchSessionStatsExtractorTest {
+    private static final String TEST_PACKAGE_NAME = "test.package.name";
+    private static final String TEST_DATABASE = "database";
+
+    @Test
+    public void testExtract() {
+        // Create search action and click action generic documents.
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(5000)
+                        .setQuery("test")
+                        .setFetchedResultCount(10)
+                        .build();
+        GenericDocument clickAction3 =
+                new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(6000)
+                        .setQuery("test")
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(4)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc3")
+                        .build();
+        GenericDocument clickAction4 =
+                new ClickActionGenericDocument.Builder("namespace", "click4", "builtin:ClickAction")
+                        .setCreationTimestampMillis(7000)
+                        .setQuery("test")
+                        .setResultRankInBlock(4)
+                        .setResultRankGlobal(8)
+                        .setTimeStayOnResultMillis(256)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc4")
+                        .build();
+        GenericDocument clickAction5 =
+                new ClickActionGenericDocument.Builder("namespace", "click5", "builtin:ClickAction")
+                        .setCreationTimestampMillis(8000)
+                        .setQuery("test")
+                        .setResultRankInBlock(6)
+                        .setResultRankGlobal(12)
+                        .setTimeStayOnResultMillis(2048)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc5")
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1,
+                        clickAction1,
+                        clickAction2,
+                        searchAction2,
+                        clickAction3,
+                        clickAction4,
+                        clickAction5);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).hasSize(2);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+        assertThat(searchIntentStats0.getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+        assertThat(searchIntentStats0.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+        assertThat(searchIntentStats0.getClicksStats().get(1).isGoodClick()).isFalse();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(5000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).hasSize(3);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+        assertThat(searchIntentStats1.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(searchIntentStats1.getClicksStats().get(0).isGoodClick()).isFalse();
+        assertThat(searchIntentStats1.getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+        assertThat(searchIntentStats1.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(256);
+        assertThat(searchIntentStats1.getClicksStats().get(1).isGoodClick()).isFalse();
+        assertThat(searchIntentStats1.getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+        assertThat(searchIntentStats1.getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(2048);
+        assertThat(searchIntentStats1.getClicksStats().get(2).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testExtract_noSearchActionShouldReturnEmptyList() {
+        // Create search action and click action generic documents.
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(clickAction1, clickAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void testExtract_shouldSkipUnknownActionTypeDocuments() {
+        // Create search action and click action generic documents.
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setPropertyString("query", "tes")
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .setPropertyLong("resultRankInBlock", 1)
+                        .setPropertyLong("resultRankGlobal", 2)
+                        .setPropertyLong("timeStayOnResultMillis", 512)
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc2")
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, clickAction1, clickAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(1);
+
+        // Since clickAction1 doesn't have property "actionType", it should be skipped without
+        // throwing any exception.
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("tes");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).hasSize(1);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimestampMillis()).isEqualTo(3000);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankInBlock()).isEqualTo(3);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getResultRankGlobal()).isEqualTo(6);
+        assertThat(searchIntentStats0.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+        assertThat(searchIntentStats0.getClicksStats().get(0).isGoodClick()).isFalse();
+    }
+
+    @Test
+    public void testExtract_detectAndSkipSearchNoise_appendNewCharacters() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search4", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3001)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction5 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search5", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("testing")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // searchAction2, searchAction3 should be considered as noise since they're intermediate
+        // search actions with no clicks. The extractor should create search intents only for the
+        // others.
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("testing");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_detectAndSkipSearchNoise_deleteCharacters() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("testing")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search4", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3001)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction5 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search5", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // searchAction2, searchAction3 should be considered as noise since they're intermediate
+        // search actions with no clicks. The extractor should create search intents only for the
+        // others.
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("testing");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("testing");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_occursAfterThresholdShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(3001)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, searchAction3);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // searchAction2 should not be considered as noise since it occurs after the threshold from
+        // searchAction1 (and therefore not intermediate search actions).
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(3001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_nonPrefixQueryStringShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("apple")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1500)
+                        .setQuery("application")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("email")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search4", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("google")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, searchAction3, searchAction4);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(4);
+
+        // searchAction2 and searchAction3 should not be considered as noise since neither query
+        // string is a prefix of the previous one (and therefore not intermediate search actions).
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("apple");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(1500);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("application");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("apple");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("email");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("application");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 3
+        SearchIntentStats searchIntentStats3 = searchSessionStats0.getSearchIntentsStats().get(3);
+        assertThat(searchIntentStats3.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats3.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats3.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats3.getCurrQuery()).isEqualTo("google");
+        assertThat(searchIntentStats3.getPrevQuery()).isEqualTo("email");
+        assertThat(searchIntentStats3.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats3.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(searchIntentStats3.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_lastSearchActionShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        // searchAction2 should not be considered as noise since it is the last search action (and
+        // therefore not an intermediate search action).
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_lastSearchActionOfRelatedSearchSequenceShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(0)
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(602001)
+                        .setQuery("test")
+                        .setFetchedResultCount(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, searchAction3);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        // searchAction2 should not be considered as noise:
+        // - searchAction3 is independent from searchAction2 and therefore forms an independent
+        //   search session.
+        // - So searchAction2 is the last search action of its search session (and therefore not an
+        // intermediate search action).
+        assertThat(result).hasSize(2);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(2);
+
+        SearchSessionStats searchSessionStats1 = result.get(1);
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats()).hasSize(1);
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+
+        // Search session 1, search intent 0
+        SearchIntentStats searchIntentStats2 = searchSessionStats1.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(602001);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getPrevQuery()).isNull();
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(0);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_withClickActionShouldNotBeSearchNoise() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(2000)
+                        .setQuery("te")
+                        .setFetchedResultCount(10)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2050)
+                        .setQuery("te")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc1")
+                        .build();
+        GenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setCreationTimestampMillis(10000)
+                        .setQuery("test")
+                        .setFetchedResultCount(5)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2, clickAction1, searchAction3);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(3);
+
+        // Even though searchAction2 is an intermediate search action, it should not be considered
+        // as noise since there is at least 1 valid click action associated with it.
+
+        // Search session 0, search intent 0
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 0, search intent 1
+        SearchIntentStats searchIntentStats1 = searchSessionStats0.getSearchIntentsStats().get(1);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(2000);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isEqualTo("t");
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats1.getClicksStats()).hasSize(1);
+
+        // Search session 0, search intent 2
+        SearchIntentStats searchIntentStats2 = searchSessionStats0.getSearchIntentsStats().get(2);
+        assertThat(searchIntentStats2.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats2.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats2.getTimestampMillis()).isEqualTo(10000);
+        assertThat(searchIntentStats2.getCurrQuery()).isEqualTo("test");
+        assertThat(searchIntentStats2.getPrevQuery()).isEqualTo("te");
+        assertThat(searchIntentStats2.getNumResultsFetched()).isEqualTo(5);
+        assertThat(searchIntentStats2.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(searchIntentStats2.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_independentSearchIntentShouldStartNewSearchSession() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setCreationTimestampMillis(601001)
+                        .setQuery("te")
+                        .setFetchedResultCount(10)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, searchAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        // Since time difference between searchAction1 and searchAction2 exceeds the threshold,
+        // searchAction2 should be considered as an independent search intent and therefore a new
+        // search session stats is created.
+        assertThat(result).hasSize(2);
+
+        // Search session 0
+        SearchSessionStats searchSessionStats0 = result.get(0);
+        assertThat(searchSessionStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats0.getSearchIntentsStats()).hasSize(1);
+        SearchIntentStats searchIntentStats0 = searchSessionStats0.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats0.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats0.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats0.getTimestampMillis()).isEqualTo(1000);
+        assertThat(searchIntentStats0.getCurrQuery()).isEqualTo("t");
+        assertThat(searchIntentStats0.getPrevQuery()).isNull();
+        assertThat(searchIntentStats0.getNumResultsFetched()).isEqualTo(20);
+        assertThat(searchIntentStats0.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats0.getClicksStats()).isEmpty();
+
+        // Search session 1
+        SearchSessionStats searchSessionStats1 = result.get(1);
+        assertThat(searchSessionStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats1.getSearchIntentsStats()).hasSize(1);
+        SearchIntentStats searchIntentStats1 = searchSessionStats1.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats1.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchIntentStats1.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchIntentStats1.getTimestampMillis()).isEqualTo(601001);
+        assertThat(searchIntentStats1.getCurrQuery()).isEqualTo("te");
+        assertThat(searchIntentStats1.getPrevQuery()).isNull();
+        assertThat(searchIntentStats1.getNumResultsFetched()).isEqualTo(10);
+        assertThat(searchIntentStats1.getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(searchIntentStats1.getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_shouldSetIsGoodClick() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setTimeStayOnResultMillis(2001)
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(4500)
+                        .setTimeStayOnResultMillis(1999)
+                        .build();
+        GenericDocument clickAction3 =
+                new ClickActionGenericDocument.Builder("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(7000)
+                        .setTimeStayOnResultMillis(1)
+                        .build();
+        GenericDocument clickAction4 =
+                new ClickActionGenericDocument.Builder("namespace", "click4", "builtin:ClickAction")
+                        .setCreationTimestampMillis(7500)
+                        .setTimeStayOnResultMillis(2000)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(
+                        searchAction1, clickAction1, clickAction2, clickAction3, clickAction4);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats = result.get(0);
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+        SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(4);
+
+        assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(2001);
+        assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+
+        assertThat(searchIntentStats.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1999);
+        assertThat(searchIntentStats.getClicksStats().get(1).isGoodClick()).isFalse();
+
+        assertThat(searchIntentStats.getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(1);
+        assertThat(searchIntentStats.getClicksStats().get(2).isGoodClick()).isFalse();
+
+        assertThat(searchIntentStats.getClicksStats().get(3).getTimeStayOnResultMillis())
+                .isEqualTo(2000);
+        assertThat(searchIntentStats.getClicksStats().get(3).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testExtract_unsetTimeStayOnResultShouldBeGoodClick() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, clickAction1);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats = result.get(0);
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+        SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(1);
+
+        assertThat(result).hasSize(1);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(1);
+
+        assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(0);
+        assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testExtract_nonPositiveTimeStayOnResultShouldBeGoodClick() {
+        GenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setQuery("t")
+                        .setFetchedResultCount(20)
+                        .build();
+        GenericDocument clickAction1 =
+                new ClickActionGenericDocument.Builder("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setTimeStayOnResultMillis(-1)
+                        .build();
+        GenericDocument clickAction2 =
+                new ClickActionGenericDocument.Builder("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setTimeStayOnResultMillis(0)
+                        .build();
+
+        List<GenericDocument> takenActionGenericDocuments =
+                Arrays.asList(searchAction1, clickAction1, clickAction2);
+
+        List<SearchSessionStats> result =
+                new SearchSessionStatsExtractor()
+                        .extract(TEST_PACKAGE_NAME, TEST_DATABASE, takenActionGenericDocuments);
+
+        assertThat(result).hasSize(1);
+
+        SearchSessionStats searchSessionStats = result.get(0);
+        assertThat(searchSessionStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(searchSessionStats.getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(searchSessionStats.getSearchIntentsStats()).hasSize(1);
+
+        SearchIntentStats searchIntentStats = searchSessionStats.getSearchIntentsStats().get(0);
+        assertThat(searchIntentStats.getClicksStats()).hasSize(2);
+
+        assertThat(searchIntentStats.getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(-1);
+        assertThat(searchIntentStats.getClicksStats().get(0).isGoodClick()).isTrue();
+
+        assertThat(searchIntentStats.getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(0);
+        assertThat(searchIntentStats.getClicksStats().get(1).isGoodClick()).isTrue();
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_unknown() {
+        SearchActionGenericDocument searchAction =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+        SearchActionGenericDocument searchActionWithNullQueryStr =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .build();
+
+        // Query correction type should be unknown if the current search action's query string is
+        // null.
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchActionWithNullQueryStr,
+                                /* prevSearchAction= */ null))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchActionWithNullQueryStr,
+                                /* prevSearchAction= */ searchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+
+        // Query correction type should be unknown if the previous search action contains null query
+        // string.
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction,
+                                /* prevSearchAction= */ searchActionWithNullQueryStr))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchActionWithNullQueryStr,
+                                /* prevSearchAction= */ searchActionWithNullQueryStr))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_firstQuery() {
+        SearchActionGenericDocument currSearchAction =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                currSearchAction, /* prevSearchAction= */ null))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_refinement() {
+        SearchActionGenericDocument prevSearchAction =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "baseSearch", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+
+        // Append 1 new character should be query refinement.
+        SearchActionGenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("teste")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction1, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+        // Append 2 new characters should be query refinement.
+        SearchActionGenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setQuery("tester")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction2, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+        // Backspace 1 character should be query refinement.
+        SearchActionGenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setQuery("tes")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction3, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+
+        // Backspace 1 character and append new character(s) should be query refinement.
+        SearchActionGenericDocument searchAction4 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search4", "builtin:SearchAction")
+                        .setQuery("tesla")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction4, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+    }
+
+    @Test
+    public void testGetQueryCorrectionType_abandonment() {
+        SearchActionGenericDocument prevSearchAction =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "baseSearch", "builtin:SearchAction")
+                        .setQuery("test")
+                        .build();
+
+        // Completely different query should be query abandonment.
+        SearchActionGenericDocument searchAction1 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search1", "builtin:SearchAction")
+                        .setQuery("unit")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction1, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+
+        // Backspace 2 characters should be query abandonment.
+        SearchActionGenericDocument searchAction2 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search2", "builtin:SearchAction")
+                        .setQuery("te")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction2, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+
+        // Backspace 2 characters and append new character(s) should be query abandonment.
+        SearchActionGenericDocument searchAction3 =
+                new SearchActionGenericDocument.Builder(
+                                "namespace", "search3", "builtin:SearchAction")
+                        .setQuery("texas")
+                        .build();
+        assertThat(
+                        SearchSessionStatsExtractor.getQueryCorrectionType(
+                                /* currSearchAction= */ searchAction3, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+    }
+}
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
index 483f61a..73505ea 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
@@ -89,8 +89,8 @@
                         mFile,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         // Erase overlay schemas since it doesn't exist in released V2 schema.
@@ -100,10 +100,10 @@
                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                         // no overlay schema
                         ImmutableList.of(),
-                        /*prefixedVisibilityBundles=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true, // force push the old version into disk
+                        /* prefixedVisibilityBundles= */ Collections.emptyList(),
+                        /* forceOverride= */ true, // force push the old version into disk
                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetAndroidVSchemaResponse.isSuccess()).isTrue();
 
         GetSchemaResponse getSchemaResponse =
@@ -111,7 +111,7 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         new CallerAccess(
-                                /*callingPackageName=*/ VisibilityStore.VISIBILITY_PACKAGE_NAME));
+                                /* callingPackageName= */ VisibilityStore.VISIBILITY_PACKAGE_NAME));
         assertThat(getSchemaResponse.getSchemas())
                 .containsExactly(
                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
@@ -121,7 +121,7 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                         new CallerAccess(
-                                /*callingPackageName=*/ VisibilityStore.VISIBILITY_PACKAGE_NAME));
+                                /* callingPackageName= */ VisibilityStore.VISIBILITY_PACKAGE_NAME));
         assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).isEmpty();
 
         // Build deprecated visibility documents in version 2
@@ -149,10 +149,10 @@
                         "package",
                         "database",
                         ImmutableList.of(new AppSearchSchema.Builder("Schema").build()),
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*schemaVersion=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* schemaVersion= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Put deprecated visibility documents in version 2 to AppSearchImpl
@@ -160,8 +160,8 @@
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                 visibilityDocumentV2,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Persist to disk and re-open the AppSearchImpl
         appSearchImplInV2.close();
@@ -170,8 +170,8 @@
                         mFile,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         InternalVisibilityConfig actualConfig =
@@ -180,9 +180,9 @@
                                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix + "Schema",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix + "Schema",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
 
         assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
         assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
@@ -201,7 +201,7 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         new CallerAccess(
-                                /*callingPackageName=*/ VisibilityStore.VISIBILITY_PACKAGE_NAME));
+                                /* callingPackageName= */ VisibilityStore.VISIBILITY_PACKAGE_NAME));
         assertThat(getSchemaResponse.getSchemas())
                 .containsExactly(
                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
@@ -211,7 +211,7 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                         new CallerAccess(
-                                /*callingPackageName=*/ VisibilityStore.VISIBILITY_PACKAGE_NAME));
+                                /* callingPackageName= */ VisibilityStore.VISIBILITY_PACKAGE_NAME));
         assertThat(getAndroidVOverlaySchemaResponse.getSchemas())
                 .containsExactly(VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
 
@@ -224,8 +224,8 @@
                                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                                        /*id=*/ prefix + "Schema",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ prefix + "Schema",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains("not found");
 
         appSearchImpl.close();
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
index 1218534..c8e8742 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
@@ -126,10 +126,10 @@
                         ImmutableList.of(
                                 new AppSearchSchema.Builder("schema1").build(),
                                 new AppSearchSchema.Builder("schema2").build()),
-                        /*prefixedVisibilityBundles=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*schemaVersion=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* prefixedVisibilityBundles= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* schemaVersion= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Put deprecated visibility documents in version 0 to AppSearchImpl
@@ -137,8 +137,8 @@
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                 deprecatedVisibilityDocument,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Persist to disk and re-open the AppSearchImpl
         appSearchImplInV0.close();
@@ -147,8 +147,8 @@
                         mFile,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         GenericDocument actualDocument1 =
@@ -156,15 +156,15 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                        /*id=*/ prefix + "Schema1",
-                        /*typePropertyPaths=*/ Collections.emptyMap());
+                        /* id= */ prefix + "Schema1",
+                        /* typePropertyPaths= */ Collections.emptyMap());
         GenericDocument actualDocument2 =
                 appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                        /*id=*/ prefix + "Schema2",
-                        /*typePropertyPaths=*/ Collections.emptyMap());
+                        /* id= */ prefix + "Schema2",
+                        /* typePropertyPaths= */ Collections.emptyMap());
 
         GenericDocument expectedDocument1 =
                 VisibilityToDocumentConverter.createVisibilityDocument(
@@ -241,18 +241,18 @@
                         mFile,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         InternalSetSchemaResponse internalSetSchemaResponse =
                 appSearchImpl.setSchema(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         ImmutableList.of(visibilityDocumentSchemaV0, visibilityToPackagesSchemaV0),
-                        /*prefixedVisibilityBundles=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true, // force push the old version into disk
-                        /*version=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* prefixedVisibilityBundles= */ Collections.emptyList(),
+                        /* forceOverride= */ true, // force push the old version into disk
+                        /* version= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         return appSearchImpl;
     }
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
index 38fa217..64c5e58 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
@@ -81,18 +81,18 @@
                         mFile,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         InternalSetSchemaResponse internalSetSchemaResponse =
                 appSearchImplInV1.setSchema(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         ImmutableList.of(VisibilityDocumentV1.SCHEMA),
-                        /*prefixedVisibilityBundles=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true, // force push the old version into disk
-                        /*version=*/ 1,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* prefixedVisibilityBundles= */ Collections.emptyList(),
+                        /* forceOverride= */ true, // force push the old version into disk
+                        /* version= */ 1,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         // Build deprecated visibility documents in version 1
         String prefix = PrefixUtil.createPrefix("package", "database");
@@ -115,10 +115,10 @@
                         "package",
                         "database",
                         ImmutableList.of(new AppSearchSchema.Builder("Schema").build()),
-                        /*visibilityDocuments=*/ Collections.emptyList(),
-                        /*forceOverride=*/ false,
-                        /*schemaVersion=*/ 0,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityDocuments= */ Collections.emptyList(),
+                        /* forceOverride= */ false,
+                        /* schemaVersion= */ 0,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
 
         // Put deprecated visibility documents in version 0 to AppSearchImpl
@@ -126,8 +126,8 @@
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                 visibilityDocumentV1,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // Persist to disk and re-open the AppSearchImpl
         appSearchImplInV1.close();
@@ -136,8 +136,8 @@
                         mFile,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
 
         InternalVisibilityConfig actualConfig =
@@ -146,9 +146,9 @@
                                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ prefix + "Schema",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ prefix + "Schema",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
 
         assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
         assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreTest.java
index 6e4f8f2..311e4e4 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityStoreTest.java
@@ -68,8 +68,8 @@
                         appSearchDir,
                         new AppSearchConfigImpl(
                                 new UnlimitedLimitConfig(), new LocalStorageIcingOptionsConfig()),
-                        /*initStatsBuilder=*/ null,
-                        /*visibilityChecker=*/ null,
+                        /* initStatsBuilder= */ null,
+                        /* visibilityChecker= */ null,
                         ALWAYS_OPTIMIZE);
         mVisibilityStore = new VisibilityStore(mAppSearchImpl);
     }
@@ -141,8 +141,8 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                        /*id=*/ prefix + "Email",
-                        /*typePropertyPaths=*/ Collections.emptyMap());
+                        /* id= */ prefix + "Email",
+                        /* typePropertyPaths= */ Collections.emptyMap());
         // Ignore the creation timestamp
         actualDocument =
                 new GenericDocument.Builder<>(actualDocument).setCreationTimestampMillis(0).build();
@@ -169,9 +169,9 @@
                                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                                 VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                /*id=*/ "Email",
-                                /*typePropertyPaths=*/ Collections.emptyMap()),
-                        /*androidVOverlayDocument=*/ null);
+                                /* id= */ "Email",
+                                /* typePropertyPaths= */ Collections.emptyMap()),
+                        /* androidVOverlayDocument= */ null);
         assertThat(actualConfig).isEqualTo(visibilityConfig);
 
         mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityConfig.getSchemaType()));
@@ -185,8 +185,8 @@
                                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
-                                        /*id=*/ "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains("Document (VS#Pkg$VS#Db/, Email) not found.");
     }
 
@@ -205,10 +205,10 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
                         Collections.singletonList(brokenSchema),
-                        /*visibilityConfigs=*/ Collections.emptyList(),
-                        /*forceOverride=*/ true,
-                        /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
-                        /*setSchemaStatsBuilder=*/ null);
+                        /* visibilityConfigs= */ Collections.emptyList(),
+                        /* forceOverride= */ true,
+                        /* version= */ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                        /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         // Create VisibilityStore should recover the broken schema
         mVisibilityStore = new VisibilityStore(mAppSearchImpl);
@@ -248,8 +248,8 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                        /*id=*/ prefix + "Email",
-                        /*typePropertyPaths=*/ Collections.emptyMap());
+                        /* id= */ prefix + "Email",
+                        /* typePropertyPaths= */ Collections.emptyMap());
         // Ignore the creation timestamp
         visibleToConfigOverlay =
                 new GenericDocument.Builder<>(visibleToConfigOverlay)
@@ -268,8 +268,8 @@
                                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                                        /*id=*/ prefix + "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ prefix + "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains("not found.");
     }
 
@@ -295,8 +295,8 @@
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                 fakeAndroidVOverlay,
-                /*sendChangeNotifications=*/ false,
-                /*logger=*/ null);
+                /* sendChangeNotifications= */ false,
+                /* logger= */ null);
 
         // update the visibility config w/o overlay
         InternalVisibilityConfig updateConfig =
@@ -312,8 +312,8 @@
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                        /*id=*/ "Email",
-                        /*typePropertyPaths=*/ Collections.emptyMap());
+                        /* id= */ "Email",
+                        /* typePropertyPaths= */ Collections.emptyMap());
 
         // Ignore the creation timestamp
         actualAndroidVOverlay =
@@ -339,8 +339,8 @@
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                 VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                /*id=*/ "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap());
+                /* id= */ "Email",
+                /* typePropertyPaths= */ Collections.emptyMap());
 
         // update the visibility config w/o overlay
         InternalVisibilityConfig updateConfig =
@@ -356,8 +356,8 @@
                                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                                        /*id=*/ "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains("not found.");
     }
 
@@ -379,8 +379,8 @@
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                 VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                /*id=*/ "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap());
+                /* id= */ "Email",
+                /* typePropertyPaths= */ Collections.emptyMap());
 
         // update the visibility config w/o overlay
         InternalVisibilityConfig updateConfig =
@@ -396,8 +396,8 @@
                                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                                         VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
                                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
-                                        /*id=*/ "Email",
-                                        /*typePropertyPaths=*/ Collections.emptyMap()));
+                                        /* id= */ "Email",
+                                        /* typePropertyPaths= */ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains("not found.");
     }
 
@@ -411,10 +411,10 @@
                         VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
                         VisibilityPermissionConfig.SCHEMA,
                         VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA),
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ true,
-                /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
-                /*setSchemaStatsBuilder=*/ null);
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ true,
+                /* version= */ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
+                /* setSchemaStatsBuilder= */ null);
 
         // Create VisibilityStore with success and force remove deprecated public acl schema from
         // the main visibility database.
@@ -501,11 +501,11 @@
                         deprecatedOverlaySchema,
                         deprecatedVisibleToConfigSchema,
                         VisibilityPermissionConfig.SCHEMA),
-                /*visibilityConfigs=*/ Collections.emptyList(),
-                /*forceOverride=*/ true,
-                /*version=*/ VisibilityToDocumentConverter
+                /* visibilityConfigs= */ Collections.emptyList(),
+                /* forceOverride= */ true,
+                /* version= */ VisibilityToDocumentConverter
                         .OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG,
-                /*setSchemaStatsBuilder=*/ null);
+                /* setSchemaStatsBuilder= */ null);
 
         // Create VisibilityStore with success and force remove override overlay schema.
         mVisibilityStore = new VisibilityStore(mAppSearchImpl);
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
index e6fa0fc..3dc4369 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
@@ -313,10 +313,10 @@
         SetSchemaRequest setSchemaRequest =
                 new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("someSchema").build())
-                        .setSchemaTypeDisplayedBySystem("someSchema", /*displayed=*/ true)
+                        .setSchemaTypeDisplayedBySystem("someSchema", /* displayed= */ true)
                         .setSchemaTypeVisibilityForPackage(
                                 "someSchema",
-                                /*visible=*/ true,
+                                /* visible= */ true,
                                 new PackageIdentifier("com.example.test6", cert6))
                         .addRequiredPermissionsForSchemaTypeVisibility(
                                 "someSchema", ImmutableSet.of(1, 2))
diff --git a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtilTest.java b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtilTest.java
index a67ee0c..b8d1ca1 100644
--- a/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtilTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/external/localstorage/visibilitystore/VisibilityUtilTest.java
@@ -27,18 +27,18 @@
         assertThat(
                         VisibilityUtil.isSchemaSearchableByCaller(
                                 callerAccess,
-                                /*targetPackageName=*/ "package1",
-                                /*prefixedSchema=*/ "schema",
-                                /*visibilityStore=*/ null,
-                                /*visibilityChecker=*/ null))
+                                /* targetPackageName= */ "package1",
+                                /* prefixedSchema= */ "schema",
+                                /* visibilityStore= */ null,
+                                /* visibilityChecker= */ null))
                 .isTrue();
         assertThat(
                         VisibilityUtil.isSchemaSearchableByCaller(
                                 callerAccess,
-                                /*targetPackageName=*/ "package2",
-                                /*prefixedSchema=*/ "schema",
-                                /*visibilityStore=*/ null,
-                                /*visibilityChecker=*/ null))
+                                /* targetPackageName= */ "package2",
+                                /* prefixedSchema= */ "schema",
+                                /* visibilityStore= */ null,
+                                /* visibilityChecker= */ null))
                 .isFalse();
     }
 
@@ -54,18 +54,18 @@
         assertThat(
                         VisibilityUtil.isSchemaSearchableByCaller(
                                 callerAccess,
-                                /*targetPackageName=*/ "package1",
-                                /*prefixedSchema=*/ "schema",
-                                /*visibilityStore=*/ null,
-                                /*visibilityChecker=*/ null))
+                                /* targetPackageName= */ "package1",
+                                /* prefixedSchema= */ "schema",
+                                /* visibilityStore= */ null,
+                                /* visibilityChecker= */ null))
                 .isFalse();
         assertThat(
                         VisibilityUtil.isSchemaSearchableByCaller(
                                 callerAccess,
-                                /*targetPackageName=*/ "package2",
-                                /*prefixedSchema=*/ "schema",
-                                /*visibilityStore=*/ null,
-                                /*visibilityChecker=*/ null))
+                                /* targetPackageName= */ "package2",
+                                /* prefixedSchema= */ "schema",
+                                /* visibilityStore= */ null,
+                                /* visibilityChecker= */ null))
                 .isFalse();
     }
 }
diff --git a/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityCheckerImplTest.java b/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityCheckerImplTest.java
index a9f32cf..eb9c102 100644
--- a/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityCheckerImplTest.java
+++ b/testing/servicestests/src/com/android/server/appsearch/visibilitystore/VisibilityCheckerImplTest.java
@@ -86,7 +86,8 @@
     @Before
     public void setUp() throws Exception {
         Context context = ApplicationProvider.getApplicationContext();
-        mAttributionSource = AppSearchAttributionSource.createAttributionSource(context);
+        mAttributionSource = AppSearchAttributionSource.createAttributionSource(context,
+                /* callingPid= */ 1);
         mContext = new ContextWrapper(context) {
             @Override
             public Context createContextAsUser(UserHandle user, int flags) {
@@ -184,11 +185,13 @@
         String packageNameFoo = "packageFoo";
         byte[] sha256CertFoo = new byte[32];
         int uidFoo = 1;
+        int pidFoo = 1;
 
         // Values for a "bar" client
         String packageNameBar = "packageBar";
         byte[] sha256CertBar = new byte[32];
         int uidBar = 2;
+        int pidBar = 2;
 
         // Can't be the same value as uidFoo nor uidBar
         int uidNotFooOrBar = 3;
@@ -211,7 +214,8 @@
                 packageNameFoo, sha256CertFoo, PackageManager.CERT_INPUT_SHA256))
                 .thenReturn(false);
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
-                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo),
+                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo,
+                        pidFoo),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false),
                 "package",
                 "prefix/SchemaFoo",
@@ -225,7 +229,8 @@
                 packageNameFoo, sha256CertFoo, PackageManager.CERT_INPUT_SHA256))
                 .thenReturn(true);
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
-                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo),
+                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo,
+                        pidFoo),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false),
                 "package",
                 "prefix/SchemaFoo",
@@ -239,7 +244,8 @@
                 packageNameFoo, sha256CertFoo, PackageManager.CERT_INPUT_SHA256))
                 .thenReturn(true);
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
-                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo),
+                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo,
+                        pidFoo),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false),
                 "package",
                 "prefix/SchemaFoo",
@@ -252,7 +258,8 @@
                 packageNameBar, sha256CertBar, PackageManager.CERT_INPUT_SHA256))
                 .thenReturn(true);
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
-                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameBar, uidBar),
+                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameBar, uidBar,
+                        pidBar),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false),
                 "package",
                 "prefix/SchemaBar",
@@ -264,14 +271,16 @@
         visibilityConfig2 = new InternalVisibilityConfig.Builder(/*id=*/"prefix/SchemaBar").build();
         mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig1, visibilityConfig2));
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
-                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo),
+                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameFoo, uidFoo,
+                        pidBar),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false),
                 "package",
                 "prefix/SchemaFoo",
                 mVisibilityStore))
                 .isFalse();
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
-                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameBar, uidBar),
+                new FrameworkCallerAccess(new AppSearchAttributionSource(packageNameBar, uidBar,
+                        pidBar),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false),
                 "package",
                 "prefix/SchemaBar",
@@ -894,13 +903,16 @@
         mVisibilityStore.setVisibility(visibilityConfigs);
 
         FrameworkCallerAccess callerAccessA =
-                new FrameworkCallerAccess(new AppSearchAttributionSource("A", 1),
+                new FrameworkCallerAccess(new AppSearchAttributionSource("A", 1,
+                        /* callingPid= */ 1),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false);
         FrameworkCallerAccess callerAccessB =
-                new FrameworkCallerAccess(new AppSearchAttributionSource("B", 2),
+                new FrameworkCallerAccess(new AppSearchAttributionSource("B", 2,
+                        /* callingPid= */ 2),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false);
         FrameworkCallerAccess callerAccessC =
-                new FrameworkCallerAccess(new AppSearchAttributionSource("C", 3),
+                new FrameworkCallerAccess(new AppSearchAttributionSource("C", 3,
+                        /* callingPid= */ 3),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false);
 
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(
@@ -961,10 +973,12 @@
         mVisibilityStore.setVisibility(visibilityConfigs);
 
         FrameworkCallerAccess callerAccessA =
-                new FrameworkCallerAccess(new AppSearchAttributionSource("A", 1),
+                new FrameworkCallerAccess(new AppSearchAttributionSource("A", 1,
+                        /* callingPid= */ 1),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false);
         FrameworkCallerAccess callerAccessB =
-                new FrameworkCallerAccess(new AppSearchAttributionSource("B", 2),
+                new FrameworkCallerAccess(new AppSearchAttributionSource("B", 2,
+                        /* callingPid= */ 2),
                         /*callerHasSystemAccess=*/ false, /*isForEnterprise=*/ false);
 
         assertThat(mVisibilityChecker.isSchemaSearchableByCaller(