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 -> { * if (appSearchResult.isSuccess()) { - * //Schema has been successfully set. + * // Schema has been successfully set. * } * });</pre> * @@ -105,7 +107,7 @@ * .build(); * mAppSearchSession.put(request, mExecutor, appSearchBatchResult -> { * 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}<{@link EnterpriseGlobalSearchSession}> - * of performing this operation. Or a {@link AppSearchResult} with failure reason code and + * @param callback The {@link AppSearchResult}<{@link EnterpriseGlobalSearchSession}> 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}<{@link AppSearchSession}> 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<String>) + * <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}<{@link String}>. + */ + 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}<{@link MigrationFailure}>. + */ + 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}<{@link SearchSuggestionResult}>. + */ + 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}<{@link Void}>. */ 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(