Merge "Revert "Add an open() overload that receives open flags to bundled driver."" into androidx-main am: 45d98ad557

Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/3110024

Change-Id: Ia97b40229c8bb3b400b1de804f94a8e936807dbc
Signed-off-by: Automerger Merge Worker 
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index faad5d3..7c97539 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -15,6 +15,7 @@
 }
 
 android {
+    compileSdkPreview "VanillaIceCream"
     defaultConfig {
         multiDexEnabled true
     }
diff --git a/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt b/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
index 004ca84..d24f8b4 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/EdgeToEdgeTest.kt
@@ -33,6 +33,7 @@
 @RunWith(AndroidJUnit4::class)
 class EdgeToEdgeTest {
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableAuto() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -73,6 +74,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableCustom() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -117,6 +119,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableDark() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
@@ -153,6 +156,7 @@
         }
     }
 
+    @Suppress("DEPRECATION")
     @Test
     fun enableLight() {
         withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt b/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
index 4f78044..73953fb 100644
--- a/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
+++ b/activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt
@@ -254,6 +254,7 @@
 @RequiresApi(23)
 private class EdgeToEdgeApi23 : EdgeToEdgeBase() {
 
+    @Suppress("DEPRECATION")
     @DoNotInline
     override fun setUp(
         statusBarStyle: SystemBarStyle,
@@ -273,6 +274,7 @@
 @RequiresApi(26)
 private open class EdgeToEdgeApi26 : EdgeToEdgeBase() {
 
+    @Suppress("DEPRECATION")
     @DoNotInline
     override fun setUp(
         statusBarStyle: SystemBarStyle,
@@ -305,6 +307,7 @@
 @RequiresApi(29)
 private open class EdgeToEdgeApi29 : EdgeToEdgeApi28() {
 
+    @Suppress("DEPRECATION")
     @DoNotInline
     override fun setUp(
         statusBarStyle: SystemBarStyle,
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
index 9d17a58..d98a917 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/app/ShortcutAdapter.java
@@ -23,11 +23,13 @@
 import android.os.Bundle;
 import android.text.TextUtils;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.util.Preconditions;
 
@@ -61,6 +63,9 @@
             + "Please use androidx.appsearch.app.ShortcutAdapter.DEFAULT_NAMESPACE as the "
             + "namespace of the document if it will be used to create a shortcut.";
 
+    private static final String APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE =
+            "appsearch_generic_doc_parcel";
+
     /**
      * Converts given document to a {@link ShortcutInfoCompat.Builder}, which can be used to
      * construct a shortcut for donation through
@@ -117,16 +122,19 @@
             throw new IllegalArgumentException(NAMESPACE_CHECK_ERROR_MESSAGE);
         }
         final String name = doc.getPropertyString(FIELD_NAME);
+        final Bundle extras = new Bundle();
+        extras.putParcelable(APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, doc.getDocumentParcel());
         return new ShortcutInfoCompat.Builder(context, doc.getId())
                 .setShortLabel(!TextUtils.isEmpty(name) ? name : doc.getId())
                 .setIntent(new Intent(Intent.ACTION_VIEW, getDocumentUri(doc)))
                 .setExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)
-                .setTransientExtras(doc.getBundle());
+                .setTransientExtras(extras);
     }
 
     /**
      * Extracts {@link GenericDocument} from given {@link ShortcutInfoCompat} if applicable.
      * Returns null if document cannot be found in the given shortcut.
+     *
      * @exportToFramework:hide
      */
     @Nullable
@@ -137,7 +145,21 @@
         if (extras == null) {
             return null;
         }
-        return new GenericDocument(extras);
+
+        GenericDocumentParcel genericDocParcel;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            genericDocParcel = Api33Impl.getParcelableFromBundle(extras,
+                    APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE, GenericDocumentParcel.class);
+        } else {
+            @SuppressWarnings("deprecation")
+            GenericDocumentParcel tmp = (GenericDocumentParcel) extras.getParcelable(
+                    APPSEARCH_GENERIC_DOC_PARCEL_NAME_IN_BUNDLE);
+            genericDocParcel = tmp;
+        }
+        if (genericDocParcel == null) {
+            return null;
+        }
+        return new GenericDocument(genericDocParcel);
     }
 
     /**
@@ -177,4 +199,21 @@
                 .path(DEFAULT_NAMESPACE + "/" + id)
                 .build();
     }
+    @RequiresApi(33)
+    static class Api33Impl {
+        private Api33Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static  T getParcelableFromBundle(
+                @NonNull Bundle bundle,
+                @NonNull String key,
+                @NonNull Class clazz) {
+            Preconditions.checkNotNull(bundle);
+            Preconditions.checkNotNull(key);
+            Preconditions.checkNotNull(clazz);
+            return bundle.getParcelable(key, clazz);
+        }
+    }
 }
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
index 2c40844..f84153b 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/properties/Keyword.java
@@ -73,6 +73,6 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mAsText);
+        return Objects.hashCode(mAsText);
     }
 }
diff --git a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
index 5d84149..17f6a11 100644
--- a/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
+++ b/appsearch/appsearch-debug-view/samples/src/main/java/androidx/appsearch/debugview/samples/NotesActivity.java
@@ -31,6 +31,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.debugview.samples.model.Note;
 import androidx.appsearch.debugview.view.AppSearchDebugActivity;
 import androidx.core.content.ContextCompat;
@@ -49,7 +50,6 @@
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Executors;
 
 /**
  * Default Activity for AppSearch Debug View Sample App
@@ -80,7 +80,8 @@
         mListView = findViewById(R.id.list_view);
         mLoadingView = findViewById(R.id.text_view);
 
-        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(AppSearchEnvironmentFactory
+                .getEnvironmentInstance().createCachedThreadPoolExecutor());
 
         mNotesAppSearchManagerFuture.setFuture(NotesAppSearchManager.createNotesAppSearchManager(
                 getApplicationContext(), mBackgroundExecutor));
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
index a6a79da..79a4c6b 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/DebugAppSearchManager.java
@@ -165,7 +165,7 @@
         SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
                 .setResultCountPerPage(PAGE_SIZE)
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList());
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList());
         String retrieveAllQueryString = "";
 
         if (mSearchType == AppSearchDebugActivity.SEARCH_TYPE_GLOBAL) {
diff --git a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
index 8c979f8..08320075 100644
--- a/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
+++ b/appsearch/appsearch-debug-view/src/main/java/androidx/appsearch/debugview/view/AppSearchDebugActivity.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.debugview.DebugAppSearchManager;
 import androidx.appsearch.debugview.R;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -36,7 +37,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.Executors;
 
 /**
  * Debug Activity for AppSearch.
@@ -105,7 +105,8 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_appsearchdebug);
 
-        mBackgroundExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        mBackgroundExecutor = MoreExecutors.listeningDecorator(AppSearchEnvironmentFactory
+                .getEnvironmentInstance().createCachedThreadPoolExecutor());
         mDbName = getIntent().getExtras().getString(DB_INTENT_KEY);
         String targetPackageName =
                 getIntent().getExtras().getString(TARGET_PACKAGE_NAME_INTENT_KEY);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 3833958..ae89b19 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -20,6 +20,10 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.addPrefixToDocument;
 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.VISIBILITY_DATABASE_NAME;
+import static androidx.appsearch.localstorage.visibilitystore.VisibilityStore.VISIBILITY_PACKAGE_NAME;
+import static androidx.appsearch.testutil.AppSearchTestUtils.createMockVisibilityChecker;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -27,11 +31,13 @@
 
 import android.content.Context;
 
+import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SearchResult;
@@ -41,7 +47,6 @@
 import androidx.appsearch.app.SearchSuggestionSpec;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.stats.InitializeStats;
 import androidx.appsearch.localstorage.stats.OptimizeStats;
@@ -49,6 +54,7 @@
 import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityToDocumentConverter;
 import androidx.appsearch.observer.DocumentChangeInfo;
 import androidx.appsearch.observer.ObserverSpec;
 import androidx.appsearch.observer.SchemaChangeInfo;
@@ -58,6 +64,9 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.FlakyTest;
 
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
 import com.google.android.icing.proto.DebugInfoProto;
 import com.google.android.icing.proto.DebugInfoVerbosity;
 import com.google.android.icing.proto.DocumentProto;
@@ -72,7 +81,9 @@
 import com.google.android.icing.proto.StorageInfoProto;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
+import com.google.android.icing.protobuf.ByteString;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -117,8 +128,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
     }
 
     @After
@@ -400,7 +411,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -454,7 +465,7 @@
                 mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -502,7 +513,7 @@
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
 
         // Check recovery state
         InitializeStats initStats = initStatsBuilder.build();
@@ -539,7 +550,7 @@
                 mContext.getPackageName(),
                 "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -585,7 +596,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -598,7 +609,7 @@
                 "package2",
                 "database2",
                 schema2,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -650,7 +661,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -663,7 +674,7 @@
                 "package2",
                 "database2",
                 schema2,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -727,8 +738,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         // We need to share across packages
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -736,8 +746,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Insert package1 schema
         List personSchema =
@@ -746,7 +756,7 @@
                 "package1",
                 "database1",
                 personSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -764,7 +774,7 @@
                 "package2",
                 "database2",
                 callSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -777,7 +787,7 @@
                 "package3",
                 "database3",
                 textSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -903,8 +913,7 @@
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
         // We need to share across packages
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -912,8 +921,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         AppSearchSchema.StringPropertyConfig personField =
                 new AppSearchSchema.StringPropertyConfig.Builder("personId")
@@ -931,7 +940,7 @@
                 "package1",
                 "database1",
                 personAndCallSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -945,7 +954,7 @@
                 "package2",
                 "database2",
                 callSchema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1107,7 +1116,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1166,7 +1175,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1215,7 +1224,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1273,7 +1282,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1334,7 +1343,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1392,7 +1401,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1423,7 +1432,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1476,7 +1485,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1539,7 +1548,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1595,7 +1604,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1661,7 +1670,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1721,7 +1730,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1765,7 +1774,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1828,7 +1837,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1891,7 +1900,7 @@
                 "package1",
                 "database1",
                 schema1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1979,7 +1988,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -1989,7 +1998,9 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         List expectedTypes = new ArrayList<>();
@@ -2016,7 +2027,7 @@
                 "package",
                 "database1",
                 oldSchemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2031,7 +2042,7 @@
                 "package",
                 "database1",
                 newSchemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2055,7 +2066,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2065,9 +2076,14 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database1/Document")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         // Check both schema Email and Document saved correctly.
@@ -2083,7 +2099,7 @@
                         "package",
                         "database1",
                         finalSchemas,
-                        /*visibilityDocuments=*/ Collections.emptyList(),
+                        /*visibilityConfigs=*/ Collections.emptyList(),
                         /*forceOverride=*/ false,
                         /*version=*/ 0,
                         /* setSchemaStatsBuilder= */ null);
@@ -2098,7 +2114,7 @@
                 "package",
                 "database1",
                 finalSchemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2108,7 +2124,9 @@
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         expectedTypes = new ArrayList<>();
@@ -2133,7 +2151,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2142,7 +2160,7 @@
                 "package",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2152,14 +2170,24 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database1/Document").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database2/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document").setVersion(0))
+                                .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();
 
         // Check Email and Document is saved in database 1 and 2 correctly.
@@ -2175,7 +2203,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2186,12 +2214,19 @@
         expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database1/Email").setVersion(0))
+                                .setSchemaType("package$database1/Email")
+                                .setDescription("")
+                                .setVersion(0))
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("package$database2/Email").setVersion(0))
-                .addTypes(SchemaTypeConfigProto.newBuilder().setSchemaType(
-                        "package$database2/Document").setVersion(0))
+                                .setSchemaType("package$database2/Email")
+                                .setDescription("")
+                                .setVersion(0))
+                .addTypes(
+                        SchemaTypeConfigProto.newBuilder()
+                                .setSchemaType("package$database2/Document")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
 
         // Check nothing changed in database2.
@@ -2215,7 +2250,7 @@
                 "package",
                 "database",
                 schema,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2265,8 +2300,8 @@
             existingPackages.add(PrefixUtil.getPackageName(existingSchemas.get(i).getSchemaType()));
         }
 
-        // Create VisibilityDocument
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("schema")
+        // Create VisibilityConfig
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -2278,7 +2313,7 @@
                 "packageA",
                 "database",
                 schema,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2287,7 +2322,7 @@
                 "packageB",
                 "database",
                 schema,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2297,10 +2332,14 @@
         SchemaProto expectedProto = SchemaProto.newBuilder()
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("packageA$database/schema").setVersion(0))
+                                .setSchemaType("packageA$database/schema")
+                                .setDescription("")
+                                .setVersion(0))
                 .addTypes(
                         SchemaTypeConfigProto.newBuilder()
-                                .setSchemaType("packageB$database/schema").setVersion(0))
+                                .setSchemaType("packageB$database/schema")
+                                .setDescription("")
+                                .setVersion(0))
                 .build();
         List expectedTypes = new ArrayList<>();
         expectedTypes.addAll(existingSchemas);
@@ -2309,22 +2348,22 @@
                 .containsExactlyElementsIn(expectedTypes);
 
         // Verify these two visibility documents are stored in AppSearch.
-        VisibilityDocument expectedVisibilityDocumentA =
-                new VisibilityDocument.Builder("packageA$database/schema")
+        InternalVisibilityConfig expectedVisibilityConfigA =
+                new InternalVisibilityConfig.Builder("packageA$database/schema")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                         .build();
-        VisibilityDocument expectedVisibilityDocumentB =
-                new VisibilityDocument.Builder("packageB$database/schema")
+        InternalVisibilityConfig expectedVisibilityConfigB =
+                new InternalVisibilityConfig.Builder("packageB$database/schema")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                         .build();
         assertThat(mAppSearchImpl.mVisibilityStoreLocked
                 .getVisibility("packageA$database/schema"))
-                .isEqualTo(expectedVisibilityDocumentA);
+                .isEqualTo(expectedVisibilityConfigA);
         assertThat(mAppSearchImpl.mVisibilityStoreLocked
                 .getVisibility("packageB$database/schema"))
-                .isEqualTo(expectedVisibilityDocumentB);
+                .isEqualTo(expectedVisibilityConfigB);
 
         // Prune packages
         mAppSearchImpl.prunePackageData(existingPackages);
@@ -2353,7 +2392,7 @@
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package1", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2366,7 +2405,7 @@
         internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package1", "database2",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2379,7 +2418,7 @@
         internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package2", "database1",
                 Collections.singletonList(new AppSearchSchema.Builder("schema").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2401,7 +2440,7 @@
                 "package1",
                 "database1",
                 schemas1,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2410,7 +2449,7 @@
                 "package1",
                 "database2",
                 schemas2,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2419,7 +2458,7 @@
                 "package2",
                 "database1",
                 schemas3,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2429,7 +2468,8 @@
                 "package1$database2/type2",
                 "package2$database1/type3",
                 "VS#Pkg$VS#Db/VisibilityType",  // plus the stored Visibility schema
-                "VS#Pkg$VS#Db/VisibilityPermissionType");
+                "VS#Pkg$VS#Db/VisibilityPermissionType",
+                "VS#Pkg$VS#AndroidVDb/AndroidVOverlayType");
     }
 
     @FlakyTest(bugId = 204186664)
@@ -2442,7 +2482,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2541,7 +2581,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2564,7 +2604,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2585,7 +2625,7 @@
                 "package2",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2643,7 +2683,7 @@
                 "package1",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2666,7 +2706,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2688,7 +2728,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2697,7 +2737,7 @@
                 "package1",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2755,7 +2795,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2768,7 +2808,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null));
@@ -2840,7 +2880,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2870,8 +2910,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         getResult = appSearchImpl2.getDocument("package", "database", "namespace1",
                 "id1",
                 Collections.emptyMap());
@@ -2887,7 +2927,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -2942,8 +2982,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -2964,7 +3004,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3021,8 +3061,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         assertThrows(AppSearchException.class, () -> appSearchImpl2.getDocument("package",
                 "database",
                 "namespace1",
@@ -3043,7 +3083,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3077,7 +3117,7 @@
                 .isEqualTo(2);
         assertThat(
                 storageInfo.getSchemaStoreStorageInfo().getNumSchemaTypes())
-                .isEqualTo(3); // +2 for VisibilitySchema
+                .isEqualTo(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
     }
 
     @Test
@@ -3088,7 +3128,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3122,7 +3162,7 @@
                 debugInfo.getDocumentInfo().getDocumentStorageInfo().getNumAliveDocuments())
                 .isEqualTo(2);
         assertThat(debugInfo.getSchemaInfo().getSchema().getTypesList())
-                .hasSize(3); // +2 for VisibilitySchema
+                .hasSize(4); // +2 for VisibilitySchema, +1 for VisibilityOverlay
     }
 
     @Test
@@ -3146,8 +3186,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas =
@@ -3156,7 +3196,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3225,8 +3265,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas =
@@ -3235,7 +3275,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3282,8 +3322,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Make sure the limit is maintained
         e = assertThrows(AppSearchException.class, () ->
@@ -3319,8 +3359,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas =
@@ -3329,7 +3369,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3432,8 +3472,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas =
@@ -3442,7 +3482,7 @@
                 "package1",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3451,7 +3491,7 @@
                 "package1",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3460,7 +3500,7 @@
                 "package2",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3469,7 +3509,7 @@
                 "package2",
                 "database2",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3528,8 +3568,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // package1 should still be out of space
         e = assertThrows(AppSearchException.class, () ->
@@ -3585,8 +3625,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas = Collections.singletonList(
@@ -3602,7 +3642,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3738,8 +3778,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas = Collections.singletonList(
@@ -3751,7 +3791,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3821,8 +3861,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Insert schema
         List schemas = Collections.singletonList(
@@ -3834,7 +3874,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -3878,8 +3918,8 @@
                         return Integer.MAX_VALUE;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         // Index id2. This should pass but only because we check for replacements.
         mAppSearchImpl.putDocument(
@@ -3924,8 +3964,8 @@
                         return 2;
                     }
                 }, new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         AppSearchException e = assertThrows(AppSearchException.class, () ->
                 mAppSearchImpl.searchSuggestion(
@@ -3950,7 +3990,7 @@
                 mContext.getPackageName(),
                 "database1",
                 /*schemas=*/ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/false,
                 /*version=*/0,
                 /*setSchemaStatsBuilder=*/null);
@@ -4020,8 +4060,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(false);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4029,14 +4068,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4073,8 +4112,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4082,14 +4120,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4124,8 +4162,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4133,14 +4170,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4176,9 +4213,20 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) ->
-                        callerAccess.getCallingPackageName().equals("visiblePackage");
+        VisibilityChecker mockVisibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals("visiblePackage");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4186,14 +4234,14 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4237,7 +4285,7 @@
 
     @Test
     public void testSetVisibility() throws Exception {
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4249,7 +4297,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4257,27 +4305,31 @@
         String prefix = PrefixUtil.createPrefix("package", "database1");
 
         // assert the visibility document is saved.
-        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
     }
 
     @Test
     public void testSetVisibility_existingVisibilitySettingRetains() throws Exception {
         // Create Visibility Document for Email1
-        VisibilityDocument visibilityDocument1 = new VisibilityDocument.Builder("Email1")
+        InternalVisibilityConfig visibilityConfig1 = new InternalVisibilityConfig.Builder("Email1")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4289,7 +4341,7 @@
                 "package1",
                 "database",
                 schemas1,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument1),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig1),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4297,24 +4349,29 @@
         String prefix1 = PrefixUtil.createPrefix("package1", "database");
 
         // assert the visibility document is saved.
-        VisibilityDocument expectedDocument1 = new VisibilityDocument.Builder(prefix1 + "Email1")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+        InternalVisibilityConfig expectedDocument1 =
+                new InternalVisibilityConfig.Builder(prefix1 + "Email1")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix1 + "Email1"))
                 .isEqualTo(expectedDocument1);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument1 =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix1 + "Email1",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument1 =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix1 + "Email1",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
+
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
 
         // Create Visibility Document for Email2
-        VisibilityDocument visibilityDocument2 = new VisibilityDocument.Builder("Email2")
+        InternalVisibilityConfig visibilityConfig2 = new InternalVisibilityConfig.Builder("Email2")
                 .setNotDisplayedBySystem(false)
                 .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
                 .build();
@@ -4326,7 +4383,7 @@
                 "package2",
                 "database",
                 schemas2,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument2),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig2),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4334,39 +4391,46 @@
         String prefix2 = PrefixUtil.createPrefix("package2", "database");
 
         // assert the visibility document is saved.
-        VisibilityDocument expectedDocument2 = new VisibilityDocument.Builder(prefix2 + "Email2")
-                .setNotDisplayedBySystem(false)
-                .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix2 + "Email2"))
+        InternalVisibilityConfig expectedDocument2 =
+                new InternalVisibilityConfig.Builder(prefix2 + "Email2")
+                        .setNotDisplayedBySystem(false)
+                        .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix2 + "Email2"))
                 .isEqualTo(expectedDocument2);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument2 =  new VisibilityDocument.Builder(
-                mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix2 + "Email2",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument2 =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix2 + "Email2",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument2).isEqualTo(expectedDocument2);
 
         // Check the existing visibility document retains.
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix1 + "Email1"))
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix1 + "Email1"))
                 .isEqualTo(expectedDocument1);
         // Verify the VisibilityDocument is saved to AppSearchImpl.
-        actualDocument1 =  new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix1 + "Email1",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        actualDocument1 = VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                mAppSearchImpl.getDocument(
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                        /*id=*/ prefix1 + "Email1",
+                        /*typePropertyPaths=*/ Collections.emptyMap()),
+                /*androidVOverlayDocument=*/null);
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
     }
 
     @Test
     public void testSetVisibility_removeVisibilitySettings() throws Exception {
         // Create a non-all-default visibility document
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4379,26 +4443,29 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         String prefix = PrefixUtil.createPrefix("package", "database1");
-        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // Set schema Email and its all-default visibility document to AppSearch database1
@@ -4406,7 +4473,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*visibilityConfigs=*/ ImmutableList.of(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4414,12 +4481,12 @@
         // All-default visibility document won't be saved in AppSearch.
         assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
                 .isNull();
-        // Verify the VisibilityDocument is removed from AppSearchImpl.
+        // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -4429,7 +4496,7 @@
     @Test
     public void testRemoveVisibility_noRemainingSettings() throws Exception {
         // Create a non-all-default visibility document
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4442,25 +4509,29 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(visibilityDocument),
+                /*visibilityConfigs=*/ ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
         String prefix = PrefixUtil.createPrefix("package", "database1");
-        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // remove the schema and visibility setting from AppSearch
@@ -4468,7 +4539,7 @@
                 "package",
                 "database1",
                 /*schemas=*/ new ArrayList<>(),
-                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*visibilityConfigs=*/ ImmutableList.of(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4478,7 +4549,7 @@
                 "package",
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(),
+                /*visibilityConfigs=*/ ImmutableList.of(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /* setSchemaStatsBuilder= */ null);
@@ -4488,9 +4559,9 @@
         // Verify there is no visibility setting for the schema.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -4500,7 +4571,7 @@
     @Test
     public void testCloseAndReopen_visibilityInfoRetains() throws Exception {
         // set Schema and visibility to AppSearch
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
@@ -4510,7 +4581,7 @@
                 "packageName",
                 "databaseName",
                 schemas,
-                ImmutableList.of(visibilityDocument),
+                ImmutableList.of(visibilityConfig),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4525,25 +4596,29 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
-        VisibilityDocument expectedDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
+        InternalVisibilityConfig expectedDocument =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
 
-        assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email"))
+        assertThat(mAppSearchImpl.mVisibilityStoreLocked
+                .getVisibility(prefix + "Email"))
                 .isEqualTo(expectedDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        // Verify the InternalVisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualDocument =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VISIBILITY_PACKAGE_NAME,
+                                VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
         assertThat(actualDocument).isEqualTo(expectedDocument);
 
         // remove schema and visibility document
@@ -4566,16 +4641,16 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
         assertThat(mAppSearchImpl.mVisibilityStoreLocked.getVisibility(prefix + "Email")).isNull();
-        // Verify the VisibilityDocument is removed from AppSearchImpl.
+        // Verify the InternalVisibilityConfig is removed from AppSearchImpl.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VISIBILITY_PACKAGE_NAME,
+                        VISIBILITY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -4590,8 +4665,7 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> true;
+        VisibilityChecker mockVisibilityChecker = createMockVisibilityChecker(true);
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4599,16 +4673,16 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type that is not displayed by the system
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ImmutableList.of(
-                        new VisibilityDocument.Builder("Type")
+                /*visibilityConfigs=*/ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type")
                                 .setNotDisplayedBySystem(true).build()),
                 /*forceOverride=*/false,
                 /*version=*/0,
@@ -4631,7 +4705,7 @@
                 "package",
                 "database",
                 Collections.singletonList(new AppSearchSchema.Builder("Type").build()),
-                /*visibilityDocuments=*/ImmutableList.of(),
+                /*visibilityConfigs=*/ImmutableList.of(),
                 /*forceOverride=*/false,
                 /*version=*/0,
                 /*setSchemaStatsBuilder=*/null);
@@ -4655,7 +4729,7 @@
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ImmutableList.of(),
+                /*visibilityConfigs=*/ImmutableList.of(),
                 /*forceOverride=*/false,
                 /*version=*/1,
                 /*setSchemaStatsBuilder=*/null);
@@ -4687,9 +4761,20 @@
         // Create a new mAppSearchImpl with a mock Visibility Checker
         mAppSearchImpl.close();
         File tempFolder = mTemporaryFolder.newFolder();
-        VisibilityChecker mockVisibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> prefixedSchema.endsWith("VisibleType");
+        VisibilityChecker mockVisibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return prefixedSchema.endsWith("VisibleType");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
         mAppSearchImpl = AppSearchImpl.create(
                 tempFolder,
                 new AppSearchConfigImpl(
@@ -4697,19 +4782,19 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                mockVisibilityChecker);
+                mockVisibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add two schema types that are not displayed by the system.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 "package",
                 "database",
                 schemas,
-                /*visibilityDocuments=*/ImmutableList.of(
-                        new VisibilityDocument.Builder("VisibleType")
+                /*visibilityConfigs=*/ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("VisibleType")
                                 .setNotDisplayedBySystem(true)
                                 .build(),
-                        new VisibilityDocument.Builder("PrivateType")
+                        new InternalVisibilityConfig.Builder("PrivateType")
                                 .setNotDisplayedBySystem(true)
                                 .build()),
                 /*forceOverride=*/false,
@@ -4728,13 +4813,258 @@
     }
 
     @Test
+    public void testGetSchema_global_publicAcl() throws Exception {
+        List schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("PublicTypeA").build(),
+                new AppSearchSchema.Builder("PublicTypeB").build(),
+                new AppSearchSchema.Builder("PublicTypeC").build());
+
+        PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
+        PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
+        PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+
+        // Package A is visible to package B & C, package B is visible to package C (based on
+        // canPackageQuery, which we are mocking).
+        Map> packageCanSee = ImmutableMap.of(
+                "A", ImmutableSet.of("A"),
+                "B", ImmutableSet.of("A", "B"),
+                "C", ImmutableSet.of("A", "B", "C"));
+        final VisibilityChecker publicAclMockChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                InternalVisibilityConfig param = visibilityStore.getVisibility(prefixedSchema);
+                return packageCanSee.get(callerAccess.getCallingPackageName())
+                        .contains(param.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+                                .getPackageName());
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/ null,
+                publicAclMockChecker, ALWAYS_OPTIMIZE
+        );
+
+        List visibilityConfigs = ImmutableList.of(
+                new InternalVisibilityConfig.Builder("PublicTypeA")
+                        .setPubliclyVisibleTargetPackage(pkgA).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeB")
+                        .setPubliclyVisibleTargetPackage(pkgB).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeC")
+                        .setPubliclyVisibleTargetPackage(pkgC).build());
+
+        // Add the three schema types, each with their own publicly visible target package.
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                visibilityConfigs,
+                /*forceOverride=*/true,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Verify access to schemas based on calling package
+        GetSchemaResponse getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(pkgA.getPackageName()));
+        assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0));
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+
+        getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(pkgB.getPackageName()));
+        assertThat(getResponse.getSchemas()).containsExactly(schemas.get(0), schemas.get(1));
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
+
+        getResponse = mAppSearchImpl.getSchema(
+                "package",
+                "database",
+                new CallerAccess(pkgC.getPackageName()));
+        assertThat(getResponse.getSchemas()).containsExactlyElementsIn(schemas);
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeA");
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeB");
+        assertThat(getResponse.getPubliclyVisibleSchemas()).containsKey("PublicTypeC");
+    }
+
+    @Test
+    public void testGetSchema_global_publicAcl_removal() throws Exception {
+        // This test to ensure the proper documents are created through setSchema, then removed
+        // when setSchema is called again
+        List schemas = ImmutableList.of(
+                new AppSearchSchema.Builder("PublicTypeA").build(),
+                new AppSearchSchema.Builder("PublicTypeB").build(),
+                new AppSearchSchema.Builder("PublicTypeC").build());
+
+        PackageIdentifier pkgA = new PackageIdentifier("A", new byte[32]);
+        PackageIdentifier pkgB = new PackageIdentifier("B", new byte[32]);
+        PackageIdentifier pkgC = new PackageIdentifier("C", new byte[32]);
+
+        // Create a new mAppSearchImpl with a mock Visibility Checker
+        mAppSearchImpl.close();
+        File tempFolder = mTemporaryFolder.newFolder();
+
+        // Package A is visible to package B & C, package B is visible to package C (based on
+        // canPackageQuery, which we are mocking).
+        Map> packageCanSee = ImmutableMap.of(
+                "A", ImmutableSet.of("A"),
+                "B", ImmutableSet.of("A", "B"),
+                "C", ImmutableSet.of("A", "B", "C"));
+        final VisibilityChecker publicAclMockChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                InternalVisibilityConfig param = visibilityStore.getVisibility(prefixedSchema);
+                return packageCanSee.get(callerAccess.getCallingPackageName())
+                        .contains(param.getVisibilityConfig()
+                                .getPubliclyVisibleTargetPackage().getPackageName());
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
+
+        mAppSearchImpl = AppSearchImpl.create(
+                tempFolder,
+                new AppSearchConfigImpl(
+                        new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()
+                ),
+                /*initStatsBuilder=*/ null,
+                publicAclMockChecker, ALWAYS_OPTIMIZE
+        );
+
+        List visibilityConfigs = ImmutableList.of(
+                new InternalVisibilityConfig.Builder("PublicTypeA")
+                        .setPubliclyVisibleTargetPackage(pkgA).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeB")
+                        .setPubliclyVisibleTargetPackage(pkgB).build(),
+                new InternalVisibilityConfig.Builder("PublicTypeC")
+                        .setPubliclyVisibleTargetPackage(pkgC).build());
+
+        // Add two schema types that are not displayed by the system.
+        InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                visibilityConfigs,
+                /*forceOverride=*/true,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Now check for documents
+        GenericDocument visibilityOverlayA = mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeA",
+                Collections.emptyMap());
+        GenericDocument visibilityOverlayB = mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeB",
+                Collections.emptyMap());
+        GenericDocument visibilityOverlayC = mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME,
+                ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeC",
+                Collections.emptyMap());
+
+        AndroidVOverlayProto overlayProtoA = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+                        .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("A")
+                                .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+                        .build())
+                .build();
+        AndroidVOverlayProto overlayProtoB = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+                        .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("B")
+                                .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+                        .build())
+                .build();
+        AndroidVOverlayProto overlayProtoC = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(VisibilityConfigProto.newBuilder()
+                        .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("C")
+                                .setPackageSha256Cert(ByteString.copyFrom(new byte[32])).build())
+                        .build())
+                .build();
+
+        assertThat(visibilityOverlayA.getPropertyBytes("visibilityProtoSerializeProperty"))
+                .isEqualTo(overlayProtoA.toByteArray());
+        assertThat(visibilityOverlayB.getPropertyBytes("visibilityProtoSerializeProperty"))
+                .isEqualTo(overlayProtoB.toByteArray());
+        assertThat(visibilityOverlayC.getPropertyBytes("visibilityProtoSerializeProperty"))
+                .isEqualTo(overlayProtoC.toByteArray());
+
+        // now undo the "public" setting
+        visibilityConfigs = ImmutableList.of(
+                new InternalVisibilityConfig.Builder("PublicTypeA").build(),
+                new InternalVisibilityConfig.Builder("PublicTypeB").build(),
+                new InternalVisibilityConfig.Builder("PublicTypeC").build());
+
+        InternalSetSchemaResponse internalSetSchemaResponseRemoved = mAppSearchImpl.setSchema(
+                "package",
+                "database",
+                schemas,
+                visibilityConfigs,
+                /*forceOverride=*/true,
+                /*version=*/1,
+                /*setSchemaStatsBuilder=*/null);
+        assertThat(internalSetSchemaResponseRemoved.isSuccess()).isTrue();
+
+        // Now check for documents again
+        Exception e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeA", Collections.emptyMap()));
+        assertThat(e.getMessage()).endsWith("not found.");
+        e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeB", Collections.emptyMap()));
+        assertThat(e.getMessage()).endsWith("not found.");
+        e = assertThrows(AppSearchException.class, () -> mAppSearchImpl.getDocument(
+                VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                "package$database/PublicTypeC", Collections.emptyMap()));
+        assertThat(e.getMessage()).endsWith("not found.");
+    }
+
+    @Test
     public void testDispatchObserver_samePackage_noVisStore_accept() throws Exception {
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4776,8 +5106,7 @@
     @Test
     public void testDispatchObserver_samePackage_withVisStore_accept() throws Exception {
         // Make a visibility checker that rejects everything
-        final VisibilityChecker rejectChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -4786,15 +5115,15 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                rejectChecker);
+                rejectChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4840,7 +5169,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4850,7 +5179,7 @@
         TestObserverCallback observer = new TestObserverCallback();
         mAppSearchImpl.registerObserverCallback(
                 new CallerAccess(/*callingPackageName=*/
-                    "com.fake.Listening.package"),
+                        "com.fake.Listening.package"),
                 /*targetPackageName=*/mContext.getPackageName(),
                 new ObserverSpec.Builder().build(),
                 MoreExecutors.directExecutor(),
@@ -4879,9 +5208,19 @@
         final String fakeListeningPackage = "com.fake.listening.package";
 
         // Make a visibility checker that allows only fakeListeningPackage.
-        final VisibilityChecker visibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> callerAccess.getCallingPackageName().equals(fakeListeningPackage);
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals(fakeListeningPackage);
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -4890,15 +5229,15 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -4942,8 +5281,7 @@
         final String fakeListeningPackage = "com.fake.Listening.package";
 
         // Make a visibility checker that rejects everything.
-        final VisibilityChecker rejectChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore) -> false;
+        final VisibilityChecker rejectChecker = createMockVisibilityChecker(false);
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -4952,15 +5290,15 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                rejectChecker);
+                rejectChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema type
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5011,7 +5349,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5037,7 +5375,7 @@
                         new AppSearchSchema.Builder("Type1").build(),
                         new AppSearchSchema.Builder("Type2").build(),
                         new AppSearchSchema.Builder("Type3").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5062,7 +5400,7 @@
                 ImmutableList.of(
                         new AppSearchSchema.Builder("Type1").build(),
                         new AppSearchSchema.Builder("Type2").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5082,7 +5420,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type1").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5115,7 +5453,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5143,7 +5481,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 1,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5170,7 +5508,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 2,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5207,7 +5545,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5241,7 +5579,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5262,16 +5600,30 @@
         final String fakeListeningPackage = "com.fake.listening.package";
 
         // Make a fake visibility checker that actually looks at visibility store
-        final VisibilityChecker visibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> {
-                    if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
-                        return false;
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                if (!callerAccess.getCallingPackageName().equals(fakeListeningPackage)) {
+                    return false;
+                }
+
+                for (PackageIdentifier packageIdentifier :
+                        visibilityStore.getVisibility(prefixedSchema)
+                                .getVisibilityConfig().getAllowedPackages()) {
+                    if (packageIdentifier.getPackageName().equals(fakeListeningPackage)) {
+                        return true;
                     }
-                    Set allowedPackages = new ArraySet<>(
-                            visibilityStore.getVisibility(prefixedSchema).getPackageNames());
-                    return allowedPackages.contains(fakeListeningPackage);
-                };
+                }
+                return false;
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5280,8 +5632,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
@@ -5300,16 +5652,15 @@
                 mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2")
+                        new InternalVisibilityConfig.Builder("Type2")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
-                                .build()
-                ),
+                                .build()),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5330,12 +5681,12 @@
                 mContext.getPackageName(),
                 "database1",
                 schemas,
-                /*visibilityDocuments=*/ ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2").build()
+                        new InternalVisibilityConfig.Builder("Type2").build()
                 ),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
@@ -5365,13 +5716,12 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2").build()
-                ),
+                        new InternalVisibilityConfig.Builder("Type2").build()),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5396,16 +5746,15 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ImmutableList.of(
-                        new VisibilityDocument.Builder("Type1")
+                /*visibilityConfigs=*/ImmutableList.of(
+                        new InternalVisibilityConfig.Builder("Type1")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
                                 .build(),
-                        new VisibilityDocument.Builder("Type2")
+                        new InternalVisibilityConfig.Builder("Type2")
                                 .addVisibleToPackage(
                                         new PackageIdentifier(fakeListeningPackage, new byte[0]))
-                                .build()
-                ),
+                                .build()),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5426,10 +5775,20 @@
         final String fakeListeningPackage = "com.fake.listening.package";
 
         // Make a visibility checker that allows fakeListeningPackage access only to Type2.
-        final VisibilityChecker visibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> callerAccess.getCallingPackageName().equals(fakeListeningPackage)
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
                         && prefixedSchema.endsWith("Type2");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5438,8 +5797,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5460,7 +5819,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5494,7 +5853,7 @@
                                                 AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                                         .build())
                                 .build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5515,10 +5874,20 @@
         final String fakeListeningPackage = "com.fake.listening.package";
 
         // Make a visibility checker that allows fakeListeningPackage access only to Type2.
-        final VisibilityChecker visibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> callerAccess.getCallingPackageName().equals(fakeListeningPackage)
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return callerAccess.getCallingPackageName().equals(fakeListeningPackage)
                         && prefixedSchema.endsWith("Type2");
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5527,8 +5896,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5537,7 +5906,7 @@
                 ImmutableList.of(
                         new AppSearchSchema.Builder("Type1").build(),
                         new AppSearchSchema.Builder("Type2").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5557,7 +5926,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(new AppSearchSchema.Builder("Type2").build()),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5575,7 +5944,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5597,21 +5966,29 @@
 
         final String fakePackage2 = "com.fake.listening.package2";
 
-        final VisibilityChecker visibilityChecker =
-                (callerAccess, packageName, prefixedSchema, visibilityStore)
-                        -> {
-                    if (prefixedSchema.endsWith("Type1")) {
-                        return callerAccess.getCallingPackageName().equals(fakePackage1);
-                    } else if (prefixedSchema.endsWith("Type2")) {
-                        return callerAccess.getCallingPackageName().equals(fakePackage2);
-                    } else if (prefixedSchema.endsWith("Type3")) {
-                        return false;
-                    } else if (prefixedSchema.endsWith("Type4")) {
-                        return true;
-                    } else {
-                        throw new IllegalArgumentException(prefixedSchema);
-                    }
-                };
+        final VisibilityChecker visibilityChecker = new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(@NonNull CallerAccess callerAccess,
+                    @NonNull String packageName, @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                if (prefixedSchema.endsWith("Type1")) {
+                    return callerAccess.getCallingPackageName().equals(fakePackage1);
+                } else if (prefixedSchema.endsWith("Type2")) {
+                    return callerAccess.getCallingPackageName().equals(fakePackage2);
+                } else if (prefixedSchema.endsWith("Type3")) {
+                    return false;
+                } else if (prefixedSchema.endsWith("Type4")) {
+                    return true;
+                } else {
+                    throw new IllegalArgumentException(prefixedSchema);
+                }
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
+                return false;
+            }
+        };
         mAppSearchImpl.close();
         mAppSearchImpl = AppSearchImpl.create(
                 mAppSearchDir,
@@ -5620,8 +5997,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/null,
-                ALWAYS_OPTIMIZE,
-                visibilityChecker);
+                visibilityChecker, ALWAYS_OPTIMIZE
+        );
 
         // Add a schema.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
@@ -5633,7 +6010,7 @@
                         new AppSearchSchema.Builder("Type3").build(),
                         new AppSearchSchema.Builder("Type4").build()
                 ),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5669,7 +6046,7 @@
                 mContext.getPackageName(),
                 "database1",
                 ImmutableList.of(),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 0,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5724,7 +6101,7 @@
                                                 .build()
                                 ).build()
                 ),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 1,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5761,7 +6138,7 @@
                 mContext.getPackageName(),
                 "database1",
                 updatedSchemaTypes,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ false,
                 /*version=*/ 2,
                 /*setSchemaStatsBuilder=*/ null);
@@ -5783,7 +6160,7 @@
                 mContext.getPackageName(),
                 "database1",
                 updatedSchemaTypes,
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
                 /*version=*/ 3,
                 /*setSchemaStatsBuilder=*/ null);
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
index e9b8e47..aa4c96a 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchLoggerTest.java
@@ -80,8 +80,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         mLogger = new SimpleTestLogger();
     }
 
@@ -373,8 +373,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 initStatsBuilder,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
         appSearchImpl.close();
 
@@ -406,8 +406,8 @@
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         List schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
                 new AppSearchSchema.Builder("Type2").build());
@@ -443,7 +443,7 @@
         appSearchImpl = AppSearchImpl.create(
                 folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
 
         assertThat(iStats).isNotNull();
@@ -456,7 +456,8 @@
         assertThat(iStats.getDocumentStoreDataStatus()).isEqualTo(
                 InitializeStatsProto.DocumentStoreDataStatus.NO_DATA_LOSS_VALUE);
         assertThat(iStats.getDocumentCount()).isEqualTo(2);
-        assertThat(iStats.getSchemaTypeCount()).isEqualTo(4); // +2 for VisibilitySchema
+        // Type1 + Type2 +2 for VisibilitySchema, +1 for VisibilityOverlay
+        assertThat(iStats.getSchemaTypeCount()).isEqualTo(5);
         assertThat(iStats.hasReset()).isEqualTo(false);
         assertThat(iStats.getResetStatusCode()).isEqualTo(AppSearchResult.RESULT_OK);
         appSearchImpl.close();
@@ -471,7 +472,7 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(
                 folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
 
         List schemas = ImmutableList.of(
                 new AppSearchSchema.Builder("Type1").build(),
@@ -511,7 +512,7 @@
         appSearchImpl = AppSearchImpl.create(
                 folder, new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()),
-                initStatsBuilder, ALWAYS_OPTIMIZE, /*visibilityChecker=*/null);
+                initStatsBuilder, /*visibilityChecker=*/ null, ALWAYS_OPTIMIZE);
         InitializeStats iStats = initStatsBuilder.build();
 
         // Some of other fields are already covered by AppSearchImplTest#testReset()
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
index 0690cd5..f189baf 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/SearchResultsImplTest.java
@@ -55,8 +55,8 @@
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
-                /*initStatsBuilder=*/ null, ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*initStatsBuilder=*/ null, /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
     }
 
     @After
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index a8bb683..ad33c41 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -33,6 +34,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;
@@ -44,6 +46,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>(
                     "namespace", "sDocumentProperties1", SCHEMA_TYPE_1)
@@ -429,4 +435,59 @@
                 expectedDocWithParentAsMetaField);
     }
     // @exportToFramework:endStrip()
+
+    @Test
+    public void testDocumentProtoConvert_EmbeddingProperty() throws Exception {
+        GenericDocument document =
+                new 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 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 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/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 4513357..bcef397 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -21,6 +21,7 @@
 import androidx.appsearch.app.AppSearchSchema;
 
 import com.google.android.icing.proto.DocumentIndexingConfig;
+import com.google.android.icing.proto.EmbeddingIndexingConfig;
 import com.google.android.icing.proto.JoinableConfig;
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
@@ -33,6 +34,128 @@
 
 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")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
@@ -53,9 +176,11 @@
 
         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(
@@ -66,6 +191,7 @@
                         )
                 ).addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("body")
+                        .setDescription("")
                         .setDataType(PropertyConfigProto.DataType.Code.STRING)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
                         .setStringIndexingConfig(
@@ -99,9 +225,11 @@
 
         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)
                         .setStringIndexingConfig(
@@ -112,6 +240,7 @@
                         )
                 ).addProperties(PropertyConfigProto.newBuilder()
                         .setPropertyName("pubDate")
+                        .setDescription("")
                         .setDataType(PropertyConfigProto.DataType.Code.INT64)
                         .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
                 ).build();
@@ -130,28 +259,21 @@
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setJoinableValueType(AppSearchSchema.StringPropertyConfig
                                 .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        // TODO(b/274157614): Export this to framework when we can access hidden
-                        //  APIs.
-                        // @exportToFramework:startStrip()
-                        // TODO(b/274157614) start exporting this when it is unhidden in framework
-                        .setDeletionPropagation(true)
-                        // @exportToFramework:endStrip()
                         .build()
                 ).build();
 
         JoinableConfig joinableConfig = JoinableConfig.newBuilder()
                 .setValueType(JoinableConfig.ValueType.Code.QUALIFIED_ID)
-                // @exportToFramework:startStrip()
-                .setPropagateDelete(true)
-                // @exportToFramework:endStrip()
                 .build();
 
         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)
                                 .setStringIndexingConfig(StringIndexingConfig.newBuilder()
@@ -176,6 +298,7 @@
 
         SchemaTypeConfigProto expectedSchemaProto = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType("EmailMessage")
+                .setDescription("")
                 .setVersion(12345)
                 .addParentTypes("Email")
                 .addParentTypes("Message")
@@ -212,10 +335,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)
                                 .setStringIndexingConfig(StringIndexingConfig.newBuilder()
@@ -225,6 +350,7 @@
                 .addProperties(
                         PropertyConfigProto.newBuilder()
                                 .setPropertyName("worksFor")
+                                .setDescription("")
                                 .setDataType(PropertyConfigProto.DataType.Code.DOCUMENT)
                                 .setSchemaType("Organization")
                                 .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
@@ -236,4 +362,89 @@
         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/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index 778ab7a..ab674cc 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -78,8 +78,8 @@
                         mLocalStorageIcingOptionsConfig
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
     }
 
     @After
@@ -507,7 +507,7 @@
         String actionPrefix = PrefixUtil.createPrefix("aiai", "database");
 
         SearchSpec searchSpec = new SearchSpec.Builder()
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
                 .build();
 
         SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
@@ -534,7 +534,7 @@
     @Test
     public void testToResultSpecProto_projectionNoPrefixes_withWildcard() {
         SearchSpec searchSpec = new SearchSpec.Builder()
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("name"))
                 .build();
 
         SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(
@@ -555,8 +555,6 @@
         assertThat(resultSpecProto.getTypePropertyMasks(0).getPaths(0)).isEqualTo("name");
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/274157614): Export this to framework when property filters are made public
     @Test
     public void testToSearchSpecProto_propertyFilter_withJoinSpec_packageFilter() {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -609,7 +607,6 @@
         assertThat(nestedSearchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("type");
     }
 
-    // TODO(b/274157614): Export this to framework when property filters are made public
     @Test
     public void testToSearchSpecProto_propertyFilter_withWildcard() {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -635,7 +632,6 @@
         assertThat(searchSpecProto.getTypePropertyFilters(0).getPaths(0)).isEqualTo("name");
     }
 
-    // @exportToFramework:endStrip()
     @Test
     public void testToResultSpecProto_weight_withJoinSpec_packageFilter() throws Exception {
         String personPrefix = PrefixUtil.createPrefix("contacts", "database");
@@ -786,8 +782,6 @@
                         PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getNamespace()));
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/258715421) start exporting this when it is unhidden in framework
     @Test
     public void testToResultSpecProto_groupBySchema() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
@@ -834,7 +828,6 @@
                 .isEqualTo(
                     PrefixUtil.removePrefix(grouping2.getEntryGroupings(1).getSchema()));
     }
-    // @exportToFramework:endStrip()
 
     @Test
     public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
@@ -871,8 +864,6 @@
         assertThat(resultSpecProto.getResultGroupings(3).getEntryGroupingsList()).hasSize(1);
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/258715421) start exporting this when it is unhidden in framework
     @Test
     public void testToResultSpecProto_groupBySchemaAndPackage() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
@@ -1110,7 +1101,6 @@
         assertThat(grouping8.getEntryGroupings(0).getSchema())
                 .isEqualTo("package1$database/typeB");
     }
-    // @exportToFramework:endStrip()
 
     @Test
     public void testGetTargetNamespaceFilters_emptySearchingFilter() {
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
index f918784..7712923 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverterTest.java
@@ -74,9 +74,6 @@
                         .build());
     }
 
-    // @exportToFramework:startStrip()
-    // TODO(b/230553264) remove this when it is deprecated and replaced by
-    //  advanced query property filters or it is exportable.
     @Test
     public void testToProto_propertyFilters() throws Exception {
         SearchSuggestionSpec searchSuggestionSpec =
@@ -103,5 +100,4 @@
                         .addPaths("property1").addPaths("property2")
                         .build());
     }
-    // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
index 667dd86..c86d4cc 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/AppSearchStatsTest.java
@@ -227,6 +227,7 @@
         int nativeLockAcquisitionLatencyMillis = 20;
         int javaToNativeJniLatencyMillis = 21;
         int nativeToJavaJniLatencyMillis = 22;
+        String searchSourceLogTag = "tag";
         final SearchStats.Builder sStatsBuilder = new SearchStats.Builder(visibilityScope,
                 TEST_PACKAGE_NAME)
                 .setDatabase(TEST_DATA_BASE)
@@ -253,7 +254,8 @@
                 .setDocumentRetrievingLatencyMillis(nativeDocumentRetrievingLatencyMillis)
                 .setNativeLockAcquisitionLatencyMillis(nativeLockAcquisitionLatencyMillis)
                 .setJavaToNativeJniLatencyMillis(javaToNativeJniLatencyMillis)
-                .setNativeToJavaJniLatencyMillis(nativeToJavaJniLatencyMillis);
+                .setNativeToJavaJniLatencyMillis(nativeToJavaJniLatencyMillis)
+                .setSearchSourceLogTag(searchSourceLogTag);
         final SearchStats sStats = sStatsBuilder.build();
 
         assertThat(sStats.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
@@ -295,7 +297,7 @@
                 javaToNativeJniLatencyMillis);
         assertThat(sStats.getNativeToJavaJniLatencyMillis()).isEqualTo(
                 nativeToJavaJniLatencyMillis);
-
+        assertThat(sStats.getSearchSourceLogTag()).isEqualTo(searchSourceLogTag);
     }
 
     @Test
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java
new file mode 100644
index 0000000..d9e88d3
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/ClickStatsTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 androidx.appsearch.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;
+
+        final ClickStats clickStats = new ClickStats.Builder()
+                .setTimestampMillis(timestampMillis)
+                .setTimeStayOnResultMillis(timeStayOnResultMillis)
+                .setResultRankInBlock(resultRankInBlock)
+                .setResultRankGlobal(resultRankGlobal)
+                .build();
+
+        assertThat(clickStats.getTimestampMillis()).isEqualTo(timestampMillis);
+        assertThat(clickStats.getTimeStayOnResultMillis()).isEqualTo(timeStayOnResultMillis);
+        assertThat(clickStats.getResultRankInBlock()).isEqualTo(resultRankInBlock);
+        assertThat(clickStats.getResultRankGlobal()).isEqualTo(resultRankGlobal);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java
new file mode 100644
index 0000000..277123b
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/stats/SearchIntentStatsTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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 androidx.appsearch.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)
+                .build();
+        final ClickStats clickStats1 = new ClickStats.Builder()
+                .setTimestampMillis(11L)
+                .setTimeStayOnResultMillis(21L)
+                .setResultRankInBlock(31)
+                .setResultRankGlobal(41)
+                .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 testBuilder_addClicksStats_byCollection() {
+        final ClickStats clickStats0 = new ClickStats.Builder()
+                .setTimestampMillis(10L)
+                .setTimeStayOnResultMillis(20L)
+                .setResultRankInBlock(30)
+                .setResultRankGlobal(40)
+                .build();
+        final ClickStats clickStats1 = new ClickStats.Builder()
+                .setTimestampMillis(11L)
+                .setTimeStayOnResultMillis(21L)
+                .setResultRankInBlock(31)
+                .setResultRankGlobal(41)
+                .build();
+        Set 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)
+                .build();
+        final ClickStats clickStats1 = new ClickStats.Builder()
+                .setTimestampMillis(11L)
+                .setTimeStayOnResultMillis(21L)
+                .setResultRankInBlock(31)
+                .setResultRankGlobal(41)
+                .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)
+                .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)
+                .build();
+        final ClickStats clickStats1 = new ClickStats.Builder()
+                .setTimestampMillis(11L)
+                .setTimeStayOnResultMillis(21L)
+                .setResultRankInBlock(31)
+                .setResultRankGlobal(41)
+                .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)
+                .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/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.java
new file mode 100644
index 0000000..36e0ca0
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocumentTest.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 androidx.appsearch.localstorage.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.ClickAction;
+
+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);
+    }
+
+// @exportToFramework:startStrip()
+    @Test
+    public void testBuild_fromDocumentClass() throws Exception {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */1000)
+                        .setQuery("body")
+                        .setReferencedQualifiedId("pkg$db/ns#doc")
+                        .setResultRankInBlock(12)
+                        .setResultRankGlobal(34)
+                        .setTimeStayOnResultMillis(2000)
+                        .build();
+        ClickActionGenericDocument clickActionGenericDocument =
+                new ClickActionGenericDocument.Builder(
+                        GenericDocument.fromDocumentClass(clickAction)).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);
+    }
+// @exportToFramework:endStrip()
+
+    @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/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java
new file mode 100644
index 0000000..ea705b7
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocumentTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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 androidx.appsearch.localstorage.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.SearchAction;
+
+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);
+    }
+
+// @exportToFramework:startStrip()
+    @Test
+    public void testBuild_fromDocumentClass() throws Exception {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
+                        .setQuery("body")
+                        .setFetchedResultCount(123)
+                        .build();
+        SearchActionGenericDocument searchActionGenericDocument =
+                new SearchActionGenericDocument(GenericDocument.fromDocumentClass(searchAction));
+
+        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);
+    }
+// @exportToFramework:endStrip()
+
+    @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/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchIntentStatsExtractorTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchIntentStatsExtractorTest.java
new file mode 100644
index 0000000..6c9dd81
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/usagereporting/SearchIntentStatsExtractorTest.java
@@ -0,0 +1,953 @@
+/*
+ * 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 androidx.appsearch.localstorage.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.localstorage.stats.SearchIntentStats;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchIntentStatsExtractorTest {
+    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(1024)
+                .setPropertyString("referencedQualifiedId", "pkg$db/ns#doc5")
+                .build();
+
+        List takenActionGenericDocuments = Arrays.asList(
+                searchAction1, clickAction1, clickAction2,
+                searchAction2, clickAction3, clickAction4, clickAction5);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        assertThat(result).hasSize(2);
+        // Search intent 0
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("tes");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(20);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).hasSize(2);
+        assertThat(result.get(0).getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+        assertThat(result.get(0).getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+        assertThat(result.get(0).getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+        assertThat(result.get(0).getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(result.get(0).getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+        assertThat(result.get(0).getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+        assertThat(result.get(0).getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+        assertThat(result.get(0).getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+
+        // Search intent 1
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(5000);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("test");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("tes");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(10);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).hasSize(3);
+        assertThat(result.get(1).getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+        assertThat(result.get(1).getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+        assertThat(result.get(1).getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+        assertThat(result.get(1).getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(result.get(1).getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+        assertThat(result.get(1).getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+        assertThat(result.get(1).getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+        assertThat(result.get(1).getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(256);
+        assertThat(result.get(1).getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+        assertThat(result.get(1).getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+        assertThat(result.get(1).getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+        assertThat(result.get(1).getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+    }
+
+    @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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, clickAction1, clickAction2);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // Since clickAction1 doesn't have property "actionType", it should be skipped without
+        // throwing any exception.
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("tes");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(20);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).hasSize(1);
+        assertThat(result.get(0).getClicksStats().get(0).getTimestampMillis()).isEqualTo(3000);
+        assertThat(result.get(0).getClicksStats().get(0).getResultRankInBlock()).isEqualTo(3);
+        assertThat(result.get(0).getClicksStats().get(0).getResultRankGlobal()).isEqualTo(6);
+        assertThat(result.get(0).getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+    }
+
+// @exportToFramework:startStrip()
+    @Test
+    public void testExtract_builtFromDocumentClass() throws Exception {
+        SearchAction searchAction1 =
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .setQuery("tes")
+                        .setFetchedResultCount(20)
+                        .build();
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .setQuery("tes")
+                        .setReferencedQualifiedId("pkg$db/ns#doc1")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickAction clickAction2 =
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .setQuery("tes")
+                        .setReferencedQualifiedId("pkg$db/ns#doc2")
+                        .setResultRankInBlock(3)
+                        .setResultRankGlobal(6)
+                        .setTimeStayOnResultMillis(1024)
+                        .build();
+        SearchAction searchAction2 =
+                new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */5000)
+                        .setQuery("test")
+                        .setFetchedResultCount(10)
+                        .build();
+        ClickAction clickAction3 =
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */6000)
+                        .setQuery("test")
+                        .setReferencedQualifiedId("pkg$db/ns#doc3")
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(4)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickAction clickAction4 =
+                new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */7000)
+                        .setQuery("test")
+                        .setReferencedQualifiedId("pkg$db/ns#doc4")
+                        .setResultRankInBlock(4)
+                        .setResultRankGlobal(8)
+                        .setTimeStayOnResultMillis(256)
+                        .build();
+        ClickAction clickAction5 =
+                new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */8000)
+                        .setQuery("test")
+                        .setReferencedQualifiedId("pkg$db/ns#doc5")
+                        .setResultRankInBlock(6)
+                        .setResultRankGlobal(12)
+                        .setTimeStayOnResultMillis(1024)
+                        .build();
+
+        // Use PutDocumentsRequest taken action API to convert document class to GenericDocument.
+        PutDocumentsRequest putDocumentsRequest = new PutDocumentsRequest.Builder()
+                .addTakenActions(searchAction1, clickAction1, clickAction2,
+                        searchAction2, clickAction3, clickAction4, clickAction5)
+                .build();
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(
+                        putDocumentsRequest.getTakenActionGenericDocuments());
+
+        assertThat(result).hasSize(2);
+        // Search intent 0
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("tes");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(20);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).hasSize(2);
+        assertThat(result.get(0).getClicksStats().get(0).getTimestampMillis()).isEqualTo(2000);
+        assertThat(result.get(0).getClicksStats().get(0).getResultRankInBlock()).isEqualTo(1);
+        assertThat(result.get(0).getClicksStats().get(0).getResultRankGlobal()).isEqualTo(2);
+        assertThat(result.get(0).getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(result.get(0).getClicksStats().get(1).getTimestampMillis()).isEqualTo(3000);
+        assertThat(result.get(0).getClicksStats().get(1).getResultRankInBlock()).isEqualTo(3);
+        assertThat(result.get(0).getClicksStats().get(1).getResultRankGlobal()).isEqualTo(6);
+        assertThat(result.get(0).getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+
+        // Search intent 1
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(5000);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("test");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("tes");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(10);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).hasSize(3);
+        assertThat(result.get(1).getClicksStats().get(0).getTimestampMillis()).isEqualTo(6000);
+        assertThat(result.get(1).getClicksStats().get(0).getResultRankInBlock()).isEqualTo(2);
+        assertThat(result.get(1).getClicksStats().get(0).getResultRankGlobal()).isEqualTo(4);
+        assertThat(result.get(1).getClicksStats().get(0).getTimeStayOnResultMillis())
+                .isEqualTo(512);
+        assertThat(result.get(1).getClicksStats().get(1).getTimestampMillis()).isEqualTo(7000);
+        assertThat(result.get(1).getClicksStats().get(1).getResultRankInBlock()).isEqualTo(4);
+        assertThat(result.get(1).getClicksStats().get(1).getResultRankGlobal()).isEqualTo(8);
+        assertThat(result.get(1).getClicksStats().get(1).getTimeStayOnResultMillis())
+                .isEqualTo(256);
+        assertThat(result.get(1).getClicksStats().get(2).getTimestampMillis()).isEqualTo(8000);
+        assertThat(result.get(1).getClicksStats().get(2).getResultRankInBlock()).isEqualTo(6);
+        assertThat(result.get(1).getClicksStats().get(2).getResultRankGlobal()).isEqualTo(12);
+        assertThat(result.get(1).getClicksStats().get(2).getTimeStayOnResultMillis())
+                .isEqualTo(1024);
+    }
+// @exportToFramework:endStrip()
+
+    @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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // 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.
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(3001);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("test");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("t");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).isEmpty();
+
+        assertThat(result.get(2).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(2).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(2).getTimestampMillis()).isEqualTo(10000);
+        assertThat(result.get(2).getCurrQuery()).isEqualTo("testing");
+        assertThat(result.get(2).getPrevQuery()).isEqualTo("test");
+        assertThat(result.get(2).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(2).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(2).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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2, searchAction3, searchAction4, searchAction5);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // 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.
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("testing");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(3001);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("te");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("testing");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(result.get(1).getClicksStats()).isEmpty();
+
+        assertThat(result.get(2).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(2).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(2).getTimestampMillis()).isEqualTo(10000);
+        assertThat(result.get(2).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(2).getPrevQuery()).isEqualTo("te");
+        assertThat(result.get(2).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(2).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(2).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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2, searchAction3);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // searchAction2 should not be considered as noise since it occurs after the threshold from
+        // searchAction1 (and therefore not intermediate search actions).
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(3001);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("te");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("t");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).isEmpty();
+
+        assertThat(result.get(2).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(2).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(2).getTimestampMillis()).isEqualTo(10000);
+        assertThat(result.get(2).getCurrQuery()).isEqualTo("test");
+        assertThat(result.get(2).getPrevQuery()).isEqualTo("te");
+        assertThat(result.get(2).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(2).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(2).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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2, searchAction3, searchAction4);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // 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).
+        assertThat(result).hasSize(4);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("apple");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(1500);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("application");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("apple");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).isEmpty();
+
+        assertThat(result.get(2).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(2).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(2).getTimestampMillis()).isEqualTo(2000);
+        assertThat(result.get(2).getCurrQuery()).isEqualTo("email");
+        assertThat(result.get(2).getPrevQuery()).isEqualTo("application");
+        assertThat(result.get(2).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(2).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(result.get(2).getClicksStats()).isEmpty();
+
+        assertThat(result.get(3).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(3).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(3).getTimestampMillis()).isEqualTo(10000);
+        assertThat(result.get(3).getCurrQuery()).isEqualTo("google");
+        assertThat(result.get(3).getPrevQuery()).isEqualTo("email");
+        assertThat(result.get(3).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(3).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+        assertThat(result.get(3).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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // searchAction2 should not be considered as noise since it is the last search action (and
+        // therefore not an intermediate search action).
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(2000);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("te");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("t");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2, searchAction3);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // searchAction2 should not be considered as noise:
+        // - searchAction3 is independent from searchAction2.
+        // - So searchAction2 is the last search action of the related search sequence (and
+        //   therefore not an intermediate search action).
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(2000);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("te");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("t");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).isEmpty();
+
+        assertThat(result.get(2).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(2).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(2).getTimestampMillis()).isEqualTo(602001);
+        assertThat(result.get(2).getCurrQuery()).isEqualTo("test");
+        assertThat(result.get(2).getPrevQuery()).isNull();
+        assertThat(result.get(2).getNumResultsFetched()).isEqualTo(0);
+        assertThat(result.get(2).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(2).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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2, clickAction1, searchAction3);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // 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.
+        assertThat(result).hasSize(3);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(20);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(2000);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("te");
+        assertThat(result.get(1).getPrevQuery()).isEqualTo("t");
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(10);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(1).getClicksStats()).hasSize(1);
+
+        assertThat(result.get(2).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(2).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(2).getTimestampMillis()).isEqualTo(10000);
+        assertThat(result.get(2).getCurrQuery()).isEqualTo("test");
+        assertThat(result.get(2).getPrevQuery()).isEqualTo("te");
+        assertThat(result.get(2).getNumResultsFetched()).isEqualTo(5);
+        assertThat(result.get(2).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_REFINEMENT);
+        assertThat(result.get(2).getClicksStats()).isEmpty();
+    }
+
+    @Test
+    public void testExtract_detectIndependentSearchIntent() {
+        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 takenActionGenericDocuments = Arrays.asList(
+                searchAction1, searchAction2);
+
+        List result = new SearchIntentStatsExtractor(
+                TEST_PACKAGE_NAME, TEST_DATABASE).extract(takenActionGenericDocuments);
+
+        // Since time difference between searchAction1 and searchAction2 exceeds the threshold,
+        // searchAction2 should be considered as an independent search intent.
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(0).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(0).getTimestampMillis()).isEqualTo(1000);
+        assertThat(result.get(0).getCurrQuery()).isEqualTo("t");
+        assertThat(result.get(0).getPrevQuery()).isNull();
+        assertThat(result.get(0).getNumResultsFetched()).isEqualTo(20);
+        assertThat(result.get(0).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(0).getClicksStats()).isEmpty();
+
+        assertThat(result.get(1).getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+        assertThat(result.get(1).getDatabase()).isEqualTo(TEST_DATABASE);
+        assertThat(result.get(1).getTimestampMillis()).isEqualTo(601001);
+        assertThat(result.get(1).getCurrQuery()).isEqualTo("te");
+        assertThat(result.get(1).getPrevQuery()).isNull();
+        assertThat(result.get(1).getNumResultsFetched()).isEqualTo(10);
+        assertThat(result.get(1).getQueryCorrectionType())
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_FIRST_QUERY);
+        assertThat(result.get(1).getClicksStats()).isEmpty();
+    }
+
+    @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(SearchIntentStatsExtractor.getQueryCorrectionType(
+                    /* currSearchAction= */searchActionWithNullQueryStr,
+                    /* prevSearchAction= */null))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+        assertThat(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.getQueryCorrectionType(
+                    /* currSearchAction= */searchAction,
+                    /* prevSearchAction= */searchActionWithNullQueryStr))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_UNKNOWN);
+        assertThat(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.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(SearchIntentStatsExtractor.getQueryCorrectionType(
+                    /* currSearchAction= */searchAction3, prevSearchAction))
+                .isEqualTo(SearchIntentStats.QUERY_CORRECTION_TYPE_ABANDONMENT);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
new file mode 100644
index 0000000..e50274b
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationFromV2Test.java
@@ -0,0 +1,215 @@
+/*
+ * 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 androidx.appsearch.localstorage.visibilitystore;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.AppSearchConfigImpl;
+import androidx.appsearch.localstorage.AppSearchImpl;
+import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
+import androidx.appsearch.localstorage.OptimizeStrategy;
+import androidx.appsearch.localstorage.UnlimitedLimitConfig;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Collections;
+
+// "V2 schema" refers to V2 of the VisibilityDocument schema, but no Visibility overlay schema
+// present. Simulates backwards compatibility situations.
+public class VisibilityStoreMigrationFromV2Test {
+
+    /**
+     * Always trigger optimize in this class. OptimizeStrategy will be tested in its own test class.
+     */
+    private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
+
+    @Rule
+    public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+    private File mFile;
+
+    @Before
+    public void setUp() throws Exception {
+        // Give ourselves global query permissions
+        mFile = mTemporaryFolder.newFolder();
+    }
+
+    @Test
+    public void testVisibilityMigration_from2() throws Exception {
+        // As such, we can treat V2 documents as V3 documents when upgrading, but we need to test
+        // this.
+
+        // Values for a "foo" client
+        String packageNameFoo = "packageFoo";
+        byte[] sha256CertFoo = new byte[32];
+        PackageIdentifier packageIdentifierFoo =
+                new PackageIdentifier(packageNameFoo, sha256CertFoo);
+
+        // Values for a "bar" client
+        String packageNameBar = "packageBar";
+        byte[] sha256CertBar = new byte[32];
+        PackageIdentifier packageIdentifierBar =
+                new PackageIdentifier(packageNameBar, sha256CertBar);
+
+        // Create AppSearchImpl with visibility document version 2;
+        AppSearchImpl appSearchImplInV2 = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        // Erase overlay schemas since it doesn't exist in released V2 schema.
+        InternalSetSchemaResponse internalSetAndroidVSchemaResponse = appSearchImplInV2.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                // no overlay schema
+                ImmutableList.of(),
+                /*prefixedVisibilityBundles=*/ Collections.emptyList(),
+                /*forceOverride=*/ true, // force push the old version into disk
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(internalSetAndroidVSchemaResponse.isSuccess()).isTrue();
+
+        GetSchemaResponse getSchemaResponse = appSearchImplInV2.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+        GetSchemaResponse getAndroidVOverlaySchemaResponse = appSearchImplInV2.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).isEmpty();
+
+        // Build deprecated visibility documents in version 2
+        String prefix = PrefixUtil.createPrefix("package", "database");
+        InternalVisibilityConfig visibilityConfigV2 = new InternalVisibilityConfig.Builder(
+                prefix + "Schema")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(
+                        new PackageIdentifier(packageNameFoo, sha256CertFoo))
+                .addVisibleToPackage(
+                        new PackageIdentifier(packageNameBar, sha256CertBar))
+                .addVisibleToPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                                SetSchemaRequest.READ_CALENDAR))
+                .addVisibleToPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA))
+                .addVisibleToPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        GenericDocument visibilityDocumentV2 =
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfigV2);
+
+        // Set client schema into AppSearchImpl with empty VisibilityDocument since we need to
+        // directly put old version of VisibilityDocument.
+        InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV2.setSchema(
+                "package",
+                "database",
+                ImmutableList.of(
+                        new AppSearchSchema.Builder("Schema").build()),
+                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*forceOverride=*/ false,
+                /*schemaVersion=*/ 0,
+                /*setSchemaStatsBuilder=*/ null);
+        assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
+
+        // Put deprecated visibility documents in version 2 to AppSearchImpl
+        appSearchImplInV2.putDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                visibilityDocumentV2,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/null);
+
+        // Persist to disk and re-open the AppSearchImpl
+        appSearchImplInV2.close();
+        AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
+                new AppSearchConfigImpl(new UnlimitedLimitConfig(),
+                        new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
+
+        InternalVisibilityConfig actualConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        appSearchImpl.getDocument(
+                                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Schema",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                /*androidVOverlayDocument=*/null);
+
+        assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
+        assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(packageIdentifierFoo, packageIdentifierBar);
+        assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())
+                .containsExactlyElementsIn(ImmutableSet.of(
+                        ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA),
+                        ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
+
+        // Check that the visibility overlay schema was added.
+        getSchemaResponse = appSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+        getAndroidVOverlaySchemaResponse = appSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(/*callingPackageName=*/VisibilityStore.VISIBILITY_PACKAGE_NAME));
+        assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
+
+        // But no overlay document was created.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                 () -> appSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                         VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ prefix + "Schema",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found");
+
+        appSearchImpl.close();
+    }
+}
+
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
index 7b03a10..c91f51c 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0Test.java
@@ -29,8 +29,8 @@
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -82,19 +82,21 @@
         // "schema1" is accessible to packageFoo and "schema2" is accessible to packageBar.
         String prefix = PrefixUtil.createPrefix("package", "database");
         GenericDocument deprecatedVisibilityToPackageFoo = new GenericDocument.Builder<>(
-                VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE, "",
+                DEPRECATED_PACKAGE_SCHEMA_TYPE)
                 .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema1")
                 .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameFoo)
                 .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertFoo)
                 .build();
         GenericDocument deprecatedVisibilityToPackageBar = new GenericDocument.Builder<>(
-                VisibilityDocument.NAMESPACE, "", DEPRECATED_PACKAGE_SCHEMA_TYPE)
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE, "",
+                DEPRECATED_PACKAGE_SCHEMA_TYPE)
                 .setPropertyString(DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY, prefix + "Schema2")
                 .setPropertyString(DEPRECATED_PACKAGE_NAME_PROPERTY, packageNameBar)
                 .setPropertyBytes(DEPRECATED_SHA_256_CERT_PROPERTY, sha256CertBar)
                 .build();
         GenericDocument deprecatedVisibilityDocument = new GenericDocument.Builder<>(
-                VisibilityDocument.NAMESPACE,
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                 VisibilityStoreMigrationHelperFromV0.getDeprecatedVisibilityDocumentId(
                         "package", "database"),
                 DEPRECATED_VISIBILITY_SCHEMA_TYPE)
@@ -131,34 +133,41 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
-        VisibilityDocument actualDocument1 = new VisibilityDocument.Builder(
+        GenericDocument actualDocument1 =
                 appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Schema1",
-                        /*typePropertyPaths=*/ Collections.emptyMap())).build();
-        VisibilityDocument actualDocument2 = new VisibilityDocument.Builder(
+                        /*typePropertyPaths=*/ Collections.emptyMap());
+        GenericDocument actualDocument2 =
                 appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ prefix + "Schema2",
-                        /*typePropertyPaths=*/ Collections.emptyMap())).build();
+                        /*typePropertyPaths=*/ Collections.emptyMap());
 
-        VisibilityDocument expectedDocument1 =
-                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema1")
+        GenericDocument expectedDocument1 = VisibilityToDocumentConverter.createVisibilityDocument(
+                new InternalVisibilityConfig.Builder(prefix + "Schema1")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier(packageNameFoo, sha256CertFoo))
-                        .build();
-        VisibilityDocument expectedDocument2 =
-                new VisibilityDocument.Builder(/*id=*/ prefix + "Schema2")
+                        .build());
+        GenericDocument expectedDocument2 = VisibilityToDocumentConverter.createVisibilityDocument(
+                new InternalVisibilityConfig.Builder(prefix + "Schema2")
                         .setNotDisplayedBySystem(true)
                         .addVisibleToPackage(new PackageIdentifier(packageNameBar, sha256CertBar))
-                        .build();
+                        .build());
+
+        // Ignore the creation timestamp
+        actualDocument1 = new GenericDocument.Builder<>(actualDocument1)
+                .setCreationTimestampMillis(0).build();
+        actualDocument2 = new GenericDocument.Builder<>(actualDocument2)
+                .setCreationTimestampMillis(0).build();
+
         assertThat(actualDocument1).isEqualTo(expectedDocument1);
         assertThat(actualDocument2).isEqualTo(expectedDocument2);
         appSearchImpl.close();
@@ -197,8 +206,8 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         InternalSetSchemaResponse internalSetSchemaResponse = appSearchImpl.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
index a70da64..4b837ae 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1Test.java
@@ -23,9 +23,9 @@
 
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.LocalStorageIcingOptionsConfig;
@@ -66,17 +66,21 @@
         // Values for a "foo" client
         String packageNameFoo = "packageFoo";
         byte[] sha256CertFoo = new byte[32];
+        PackageIdentifier packageIdentifierFoo =
+                new PackageIdentifier(packageNameFoo, sha256CertFoo);
 
         // Values for a "bar" client
         String packageNameBar = "packageBar";
         byte[] sha256CertBar = new byte[32];
+        PackageIdentifier packageIdentifierBar =
+                new PackageIdentifier(packageNameBar, sha256CertBar);
 
         // Create AppSearchImpl with visibility document version 1;
         AppSearchImpl appSearchImplInV1 = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         InternalSetSchemaResponse internalSetSchemaResponse = appSearchImplInV1.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
@@ -125,24 +129,24 @@
         AppSearchImpl appSearchImpl = AppSearchImpl.create(mFile,
                 new AppSearchConfigImpl(new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()), /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
 
-        VisibilityDocument actualDocument = new VisibilityDocument.Builder(
-                appSearchImpl.getDocument(
-                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                        VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
-                        /*id=*/ prefix + "Schema",
-                        /*typePropertyPaths=*/ Collections.emptyMap())).build();
+        InternalVisibilityConfig actualConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        appSearchImpl.getDocument(
+                                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ prefix + "Schema",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                /*androidVOverlayDocument=*/null);
 
-        assertThat(actualDocument.isNotDisplayedBySystem()).isTrue();
-        assertThat(actualDocument.getPackageNames()).asList().containsExactly(packageNameFoo,
-                packageNameBar);
-        assertThat(actualDocument.getSha256Certs()).isEqualTo(
-                new byte[][] {sha256CertFoo, sha256CertBar});
-        assertThat(actualDocument.getVisibleToPermissions()).containsExactlyElementsIn(
-                ImmutableSet.of(
+        assertThat(actualConfig.isNotDisplayedBySystem()).isTrue();
+        assertThat(actualConfig.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(packageIdentifierFoo, packageIdentifierBar);
+        assertThat(actualConfig.getVisibilityConfig().getRequiredPermissions())
+                .containsExactlyElementsIn(ImmutableSet.of(
                         ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
                         ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA),
                         ImmutableSet.of(SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA)));
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
index 056acea..5ac5bda 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreTest.java
@@ -21,9 +21,13 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchConfigImpl;
 import androidx.appsearch.localstorage.AppSearchImpl;
@@ -52,22 +56,21 @@
     private static final OptimizeStrategy ALWAYS_OPTIMIZE = optimizeInfo -> true;
     @Rule
     public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
-    private File mAppSearchDir;
     private AppSearchImpl mAppSearchImpl;
     private VisibilityStore mVisibilityStore;
 
     @Before
     public void setUp() throws Exception {
-        mAppSearchDir = mTemporaryFolder.newFolder();
+        File appSearchDir = mTemporaryFolder.newFolder();
         mAppSearchImpl = AppSearchImpl.create(
-                mAppSearchDir,
+                appSearchDir,
                 new AppSearchConfigImpl(
                         new UnlimitedLimitConfig(),
                         new LocalStorageIcingOptionsConfig()
                 ),
                 /*initStatsBuilder=*/ null,
-                ALWAYS_OPTIMIZE,
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                ALWAYS_OPTIMIZE);
         mVisibilityStore = new VisibilityStore(mAppSearchImpl);
     }
 
@@ -99,55 +102,83 @@
     }
 
     @Test
-    public void testSetAndGetVisibility() throws Exception {
-        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
-                .setNotDisplayedBySystem(true)
-                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
-                .build();
-        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
-
-        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
-                .isEqualTo(visibilityDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument =
-                new VisibilityDocument.Builder(mAppSearchImpl.getDocument(
+    public void testSetVisibilitySchema() throws Exception {
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+
+
+        GetSchemaResponse getAndroidVOverlaySchemaResponse = mAppSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getAndroidVOverlaySchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
+    }
+
+    @Test
+    public void testSetAndGetVisibility() throws Exception {
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        InternalVisibilityConfig visibilityConfig =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .setNotDisplayedBySystem(true)
+                        .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                        .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+                .isEqualTo(visibilityConfig);
+        // Verify the VisibilityConfig is saved to AppSearchImpl.
+        GenericDocument actualDocument = mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                 /*id=*/ prefix + "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
-        assertThat(actualDocument).isEqualTo(visibilityDocument);
+                /*typePropertyPaths=*/ Collections.emptyMap());
+        // Ignore the creation timestamp
+        actualDocument =
+                new GenericDocument.Builder<>(actualDocument).setCreationTimestampMillis(0).build();
+
+        assertThat(actualDocument).isEqualTo(
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
     }
 
     @Test
     public void testRemoveVisibility() throws Exception {
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder("Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
-        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
 
         assertThat(mVisibilityStore.getVisibility("Email"))
-                .isEqualTo(visibilityDocument);
-        // Verify the VisibilityDocument is saved to AppSearchImpl.
-        VisibilityDocument actualDocument = new VisibilityDocument.Builder(
-                mAppSearchImpl.getDocument(
-                VisibilityStore.VISIBILITY_PACKAGE_NAME,
-                VisibilityStore.VISIBILITY_DATABASE_NAME,
-                VisibilityDocument.NAMESPACE,
-                /*id=*/ "Email",
-                /*typePropertyPaths=*/ Collections.emptyMap())).build();
-        assertThat(actualDocument).isEqualTo(visibilityDocument);
+                .isEqualTo(visibilityConfig);
+        // Verify the VisibilityConfig is saved to AppSearchImpl.
+        InternalVisibilityConfig actualConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        mAppSearchImpl.getDocument(
+                                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
+                                /*id=*/ "Email",
+                                /*typePropertyPaths=*/ Collections.emptyMap()),
+                        /*androidVOverlayDocument=*/null);
+        assertThat(actualConfig).isEqualTo(visibilityConfig);
 
-        mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityDocument.getId()));
+        mVisibilityStore.removeVisibility(ImmutableSet.of(visibilityConfig.getSchemaType()));
         assertThat(mVisibilityStore.getVisibility("Email")).isNull();
-        // Verify the VisibilityDocument is removed from AppSearchImpl.
+        // Verify the VisibilityConfig is removed from AppSearchImpl.
         AppSearchException e = assertThrows(AppSearchException.class,
                 () -> mAppSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         /*id=*/ "Email",
                         /*typePropertyPaths=*/ Collections.emptyMap()));
         assertThat(e).hasMessageThat().contains(
@@ -158,17 +189,17 @@
     public void testRecoverBrokenVisibilitySchema() throws Exception {
         // Create a broken schema which could be recovered to the latest schema in a compatible
         // change. Since we won't set force override to true to recover the broken case.
-        AppSearchSchema brokenSchema = new AppSearchSchema.Builder(VisibilityDocument.SCHEMA_TYPE)
-                .build();
+        AppSearchSchema brokenSchema = new AppSearchSchema.Builder(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA_TYPE).build();
 
         // Index a broken schema into AppSearch, use the latest version to make it broken.
         InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema(
                 VisibilityStore.VISIBILITY_PACKAGE_NAME,
                 VisibilityStore.VISIBILITY_DATABASE_NAME,
                 Collections.singletonList(brokenSchema),
-                /*visibilityDocuments=*/ Collections.emptyList(),
+                /*visibilityConfigs=*/ Collections.emptyList(),
                 /*forceOverride=*/ true,
-                /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST,
+                /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST,
                 /*setSchemaStatsBuilder=*/ null);
         assertThat(internalSetSchemaResponse.isSuccess()).isTrue();
         // Create VisibilityStore should recover the broken schema
@@ -176,13 +207,270 @@
 
         // We should be able to set and get Visibility settings.
         String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
-        VisibilityDocument visibilityDocument = new VisibilityDocument.Builder(prefix + "Email")
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder(
+                prefix + "Email")
                 .setNotDisplayedBySystem(true)
                 .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
                 .build();
-        mVisibilityStore.setVisibility(ImmutableList.of(visibilityDocument));
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
 
         assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
-                .isEqualTo(visibilityDocument);
+                .isEqualTo(visibilityConfig);
+    }
+
+    @Test
+    public void testSetGetAndRemoveOverlayVisibility() throws Exception {
+        String prefix = PrefixUtil.createPrefix("packageName", "databaseName");
+        SchemaVisibilityConfig nestedvisibilityConfig = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        InternalVisibilityConfig visibilityConfig =
+                new InternalVisibilityConfig.Builder(prefix + "Email")
+                        .addVisibleToConfig(nestedvisibilityConfig)
+                        .build();
+
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        assertThat(mVisibilityStore.getVisibility(prefix + "Email"))
+                .isEqualTo(visibilityConfig);
+        // Verify the VisibilityConfig is saved to AppSearchImpl.
+        GenericDocument visibleToConfigOverlay = mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ prefix + "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+        // Ignore the creation timestamp
+        visibleToConfigOverlay = new GenericDocument.Builder<>(visibleToConfigOverlay)
+                .setCreationTimestampMillis(0).build();
+        assertThat(visibleToConfigOverlay).isEqualTo(VisibilityToDocumentConverter
+                .createAndroidVOverlay(visibilityConfig));
+
+        mVisibilityStore.removeVisibility(ImmutableSet.of(prefix + "Email"));
+        // Verify the VisibilityConfig is removed from AppSearchImpl.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ prefix + "Email",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found.");
+    }
+
+    @Test
+    public void testSetVisibility_avoidRemoveOverlay() throws Exception {
+        // Set a visibility config w/o overlay
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        // Put a fake AndroidVOverlay into AppSearchImpl, this is not added by VisibilityStore,
+        // just add a fake AndroidVOverlay to verify we won't remove it when we update the config
+        // which doesn't contain any overlay settings.
+        GenericDocument fakeAndroidVOverlay =
+                new GenericDocument.Builder>("androidVOverlay",
+                        "Email", "AndroidVOverlayType")
+                        .setCreationTimestampMillis(0)
+                        .build();
+        mAppSearchImpl.putDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                fakeAndroidVOverlay,
+                /*sendChangeNotifications=*/ false,
+                /*logger=*/null);
+
+        // update the visibility config w/o overlay
+        InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToPackage(new PackageIdentifier("pkgFoo", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+        // Verify we won't trigger a remove() call to AppSearchImpl by get the fakeAndroidVOverlay.
+        GenericDocument actualAndroidVOverlay = mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+
+        // Ignore the creation timestamp
+        actualAndroidVOverlay = new GenericDocument.Builder<>(actualAndroidVOverlay)
+                .setCreationTimestampMillis(0).build();
+        assertThat(actualAndroidVOverlay).isEqualTo(fakeAndroidVOverlay);
+    }
+
+    @Test
+    public void testSetVisibility_removeOverlay_publicAcl() throws Exception {
+        // Set a visibility config with public overlay
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("pkgBar", new byte[32]))
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        // verify the overlay document is created.
+        mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+
+        // update the visibility config w/o overlay
+        InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+        // Verify the overlay document is removed.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ "Email",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found.");
+    }
+
+    @Test
+    public void testSetVisibility_removeOverlay_visibleToConfig() throws Exception {
+        // Set a visibility config with visible to config.
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .addVisibleToConfig(new SchemaVisibilityConfig.Builder()
+                        .addRequiredPermissions(ImmutableSet.of(1)).build())
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(visibilityConfig));
+
+        // verify the overlay document is created.
+        mAppSearchImpl.getDocument(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                /*id=*/ "Email",
+                /*typePropertyPaths=*/ Collections.emptyMap());
+
+        // update the visibility config w/o overlay
+        InternalVisibilityConfig updateConfig = new InternalVisibilityConfig.Builder("Email")
+                .setNotDisplayedBySystem(true)
+                .build();
+        mVisibilityStore.setVisibility(ImmutableList.of(updateConfig));
+
+        // Verify the overlay document is removed.
+        AppSearchException e = assertThrows(AppSearchException.class,
+                () -> mAppSearchImpl.getDocument(
+                        VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                        VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                        VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE,
+                        /*id=*/ "Email",
+                        /*typePropertyPaths=*/ Collections.emptyMap()));
+        assertThat(e).hasMessageThat().contains("not found.");
+    }
+
+    @Test
+    public void testMigrateFromDeprecatedSchema() throws Exception {
+        // Set deprecated public acl schema to main visibility database.
+        mAppSearchImpl.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                ImmutableList.of(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA,
+                VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA),
+                /*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.
+        mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.VISIBILITY_DATABASE_NAME,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA,
+                VisibilityPermissionConfig.SCHEMA);
+    }
+
+    @Test
+    public void testMigrateFromDeprecatedOverlaySchema() throws Exception {
+        // Set deprecated overlay schema to overlay database.
+        AppSearchSchema deprecatedOverlaySchema =
+                new AppSearchSchema.Builder("AndroidVOverlayType")
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackage")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackageSha256Cert")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "visibleToConfigProperty",
+                                "VisibleToConfigType")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .build();
+        AppSearchSchema deprecatedVisibleToConfigSchema =
+                new AppSearchSchema.Builder("VisibleToConfigType")
+                        .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder(
+                                "notPlatformSurfaceable")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "packageName")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                                "sha256Cert")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                "permission", VisibilityPermissionConfig.SCHEMA_TYPE)
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                .build())
+                        .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackage")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
+                                "publiclyVisibleTargetPackageSha256Cert")
+                                .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                .build())
+                        .build();
+        mAppSearchImpl.setSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                ImmutableList.of(deprecatedOverlaySchema, deprecatedVisibleToConfigSchema,
+                        VisibilityPermissionConfig.SCHEMA),
+                /*visibilityConfigs=*/ Collections.emptyList(),
+                /*forceOverride=*/ true,
+                /*version=*/ VisibilityToDocumentConverter
+                        .OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG,
+                /*setSchemaStatsBuilder=*/ null);
+
+        // Create VisibilityStore with success and force remove override overlay schema.
+        mVisibilityStore = new VisibilityStore(mAppSearchImpl);
+
+        GetSchemaResponse getSchemaResponse = mAppSearchImpl.getSchema(
+                VisibilityStore.VISIBILITY_PACKAGE_NAME,
+                VisibilityStore.ANDROID_V_OVERLAY_DATABASE_NAME,
+                new CallerAccess(VisibilityStore.VISIBILITY_PACKAGE_NAME));
+
+        assertThat(getSchemaResponse.getVersion()).isEqualTo(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST);
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(
+                VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA);
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
new file mode 100644
index 0000000..7c50c0d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverterTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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 androidx.appsearch.localstorage.visibilitystore;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.SetSchemaRequest;
+
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
+import com.google.android.appsearch.proto.VisibleToPermissionProto;
+import com.google.android.icing.protobuf.ByteString;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class VisibilityToDocumentConverterTest {
+
+    @Test
+    public void testToGenericDocuments() throws Exception {
+        // Create a SetSchemaRequest for testing
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        byte[] cert3 = new byte[32];
+        byte[] cert4 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        Arrays.fill(cert3, (byte) 3);
+        Arrays.fill(cert4, (byte) 4);
+
+        SchemaVisibilityConfig visibleToConfig = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.test2", cert2))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("someSchema").build())
+                .setSchemaTypeDisplayedBySystem("someSchema", false)
+                .setSchemaTypeVisibilityForPackage("someSchema", true,
+                        new PackageIdentifier("com.example.test3", cert3))
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        "someSchema", ImmutableSet.of(3, 4))
+                .setPubliclyVisibleSchema("someSchema",
+                        new PackageIdentifier("com.example.test4", cert4))
+                .addSchemaTypeVisibleToConfig("someSchema", visibleToConfig)
+                .build();
+
+        // Create android V overlay proto
+        VisibilityConfigProto visibleToConfigProto = VisibilityConfigProto.newBuilder()
+                .addVisibleToPackages(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test1")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert1)).build())
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                                .setPackageName("com.example.test2")
+                                .setPackageSha256Cert(ByteString.copyFrom(cert2)).build())
+                .addVisibleToPermissions(VisibleToPermissionProto.newBuilder()
+                        .addAllPermissions(ImmutableSet.of(1, 2)).build())
+                .build();
+        VisibilityConfigProto visibilityConfigProto = VisibilityConfigProto.newBuilder()
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test4")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert4)).build())
+                .build();
+        AndroidVOverlayProto overlayProto = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(visibilityConfigProto)
+                .addVisibleToConfigs(visibleToConfigProto)
+                .build();
+
+        // Create the expected AndroidVOverlay document
+        GenericDocument expectedAndroidVOverlay =
+                new GenericDocument.Builder>("androidVOverlay",
+                        "someSchema", "AndroidVOverlayType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBytes("visibilityProtoSerializeProperty",
+                                overlayProto.toByteArray())
+                        .build();
+
+        // Create the expected visibility document
+        GenericDocument permissionDoc34 =
+                new GenericDocument.Builder>("", "",
+                        "VisibilityPermissionType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyLong("allRequiredPermissions", 3, 4).build();
+        GenericDocument expectedVisibilityDocument =
+                new GenericDocument.Builder>("", "someSchema",
+                        "VisibilityType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBoolean("notPlatformSurfaceable", true)
+                        .setPropertyString("packageName", "com.example.test3")
+                        .setPropertyBytes("sha256Cert", cert3)
+                        .setPropertyDocument("permission", permissionDoc34)
+                        .build();
+
+        // Convert the SetSchemaRequest to a list of VisibilityConfig
+        List visibilityConfigs =
+                InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+
+        // Check if the conversion is correct
+        assertThat(visibilityConfigs).hasSize(1);
+        InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+
+        assertThat(expectedVisibilityDocument).isEqualTo(
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
+        assertThat(expectedAndroidVOverlay).isEqualTo(
+                VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig));
+    }
+
+    @Test
+    public void testToVisibilityConfig() throws Exception {
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        byte[] cert3 = new byte[32];
+        byte[] cert4 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        Arrays.fill(cert3, (byte) 3);
+        Arrays.fill(cert4, (byte) 4);
+
+        // Create visibility proto property
+        VisibilityConfigProto visibleToConfigProto = VisibilityConfigProto.newBuilder()
+                .addVisibleToPackages(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test1")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert1)).build())
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test2")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert2)).build())
+                .addVisibleToPermissions(VisibleToPermissionProto.newBuilder()
+                        .addAllPermissions(ImmutableSet.of(1, 2)).build())
+                .build();
+        VisibilityConfigProto visibilityConfigProto = VisibilityConfigProto.newBuilder()
+                .setPubliclyVisibleTargetPackage(PackageIdentifierProto.newBuilder()
+                        .setPackageName("com.example.test4")
+                        .setPackageSha256Cert(ByteString.copyFrom(cert4)).build())
+                .build();
+        AndroidVOverlayProto overlayProto = AndroidVOverlayProto.newBuilder()
+                .setVisibilityConfig(visibilityConfigProto)
+                .addVisibleToConfigs(visibleToConfigProto)
+                .build();
+
+        // Create a visible config overlay for testing
+        GenericDocument androidVOverlay =
+                new GenericDocument.Builder>("androidVOverlay",
+                        "someSchema", "AndroidVOverlayType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBytes("visibilityProtoSerializeProperty",
+                                overlayProto.toByteArray())
+                        .build();
+
+        // Create a VisibilityDocument for testing
+        GenericDocument permissionDoc34 =
+                new GenericDocument.Builder>("", "",
+                        "VisibilityPermissionType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyLong("allRequiredPermissions", 3, 4).build();
+        GenericDocument visibilityDoc =
+                new GenericDocument.Builder>("", "someSchema",
+                        "VisibilityType")
+                        .setCreationTimestampMillis(0)
+                        .setPropertyBoolean("notPlatformSurfaceable", true)
+                        .setPropertyString("packageName", "com.example.test3")
+                        .setPropertyBytes("sha256Cert", cert3)
+                        .setPropertyDocument("permission", permissionDoc34)
+                        .build();
+
+        // Create a VisibilityConfig using the Builder
+        InternalVisibilityConfig visibilityConfig =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        visibilityDoc, androidVOverlay);
+
+        // Check if the properties are set correctly
+        assertThat(visibilityDoc).isEqualTo(
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig));
+        GenericDocument actualOverlayDoc =
+                VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig);
+        AndroidVOverlayProto actualOverlayProto = AndroidVOverlayProto.parseFrom(
+                actualOverlayDoc.getPropertyBytes("visibilityProtoSerializeProperty"));
+        assertThat(actualOverlayProto).isEqualTo(overlayProto);
+        assertThat(androidVOverlay).isEqualTo(actualOverlayDoc);
+
+        // Verify rebuild from InternalVisibilityConfig remains the same.
+        InternalVisibilityConfig.Builder builder =
+                new InternalVisibilityConfig.Builder(visibilityConfig);
+        InternalVisibilityConfig rebuild = builder.build();
+        assertThat(visibilityConfig).isEqualTo(rebuild);
+
+        InternalVisibilityConfig modifiedConfig = builder
+                .setSchemaType("prefixedSchema")
+                .setNotDisplayedBySystem(false)
+                .addVisibleToPermissions(ImmutableSet.of(SetSchemaRequest.READ_SMS,
+                        SetSchemaRequest.READ_CALENDAR))
+                .clearVisibleToPackages()
+                .addVisibleToPackage(
+                        new PackageIdentifier("com.example.other", new byte[32]))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.other", new byte[32]))
+                .clearVisibleToConfig()
+                .build();
+        assertThat(modifiedConfig.getSchemaType()).isEqualTo("prefixedSchema");
+
+        // Check that the rebuild stayed the same
+        assertThat(rebuild.getSchemaType()).isEqualTo("someSchema");
+        assertThat(rebuild.isNotDisplayedBySystem()).isTrue();
+        assertThat(rebuild.getVisibilityConfig().getRequiredPermissions())
+                .containsExactly(ImmutableSet.of(3, 4));
+        assertThat(rebuild.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(new PackageIdentifier("com.example.test3", cert3));
+        assertThat(
+                rebuild.getVisibilityConfig().getPubliclyVisibleTargetPackage()).isEqualTo(
+                new PackageIdentifier("com.example.test4", cert4));
+
+        SchemaVisibilityConfig expectedVisibleToConfig = new SchemaVisibilityConfig.Builder()
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("com.example.test2", cert2))
+                .build();
+        assertThat(rebuild.getVisibleToConfigs()).containsExactly(expectedVisibleToConfig);
+    }
+
+    @Test
+    public void testToGenericDocumentAndBack() {
+        // Create a SetSchemaRequest for testing
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        byte[] cert3 = new byte[32];
+        byte[] cert4 = new byte[32];
+        byte[] cert5 = new byte[32];
+        byte[] cert6 = new byte[32];
+        byte[] cert7 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        Arrays.fill(cert3, (byte) 3);
+        Arrays.fill(cert4, (byte) 4);
+        Arrays.fill(cert5, (byte) 5);
+        Arrays.fill(cert6, (byte) 6);
+        Arrays.fill(cert7, (byte) 7);
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test1", cert1))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.test2", cert2))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test3", cert3))
+                .addRequiredPermissions(ImmutableSet.of(3, 4))
+                .build();
+        SchemaVisibilityConfig config3 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("com.example.test4", cert4))
+                .setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier("com.example.test5", cert5))
+                .build();
+        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("someSchema").build())
+                .setSchemaTypeDisplayedBySystem("someSchema", /*displayed=*/ true)
+                .setSchemaTypeVisibilityForPackage("someSchema", /*visible=*/ true,
+                        new PackageIdentifier("com.example.test6", cert6))
+                .addRequiredPermissionsForSchemaTypeVisibility("someSchema",
+                        ImmutableSet.of(1, 2))
+                .setPubliclyVisibleSchema("someSchema",
+                        new PackageIdentifier("com.example.test7", cert7))
+                .addSchemaTypeVisibleToConfig("someSchema", config1)
+                .addSchemaTypeVisibleToConfig("someSchema", config2)
+                .addSchemaTypeVisibleToConfig("someSchema", config3)
+                .build();
+
+        // Convert the SetSchemaRequest to a list of VisibilityConfig
+        List visibilityConfigs =
+                InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+        InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+
+        GenericDocument visibilityDoc =
+                VisibilityToDocumentConverter.createVisibilityDocument(visibilityConfig);
+        GenericDocument androidVOverlay =
+                VisibilityToDocumentConverter.createAndroidVOverlay(visibilityConfig);
+
+        InternalVisibilityConfig rebuild =
+                VisibilityToDocumentConverter.createInternalVisibilityConfig(
+                        visibilityDoc, androidVOverlay);
+
+        assertThat(rebuild).isEqualTo(visibilityConfig);
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java
new file mode 100644
index 0000000..9a98739
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtilTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 androidx.appsearch.localstorage.visibilitystore;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class VisibilityUtilTest {
+    @Test
+    public void testIsSchemaSearchableByCaller_selfAccessDefaultAllowed() {
+        CallerAccess callerAccess = new CallerAccess("package1");
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package1",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isTrue();
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package2",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isFalse();
+    }
+
+    @Test
+    public void testIsSchemaSearchableByCaller_selfAccessNotAllowed() {
+        CallerAccess callerAccess = new CallerAccess("package1") {
+            @Override
+            public boolean doesCallerHaveSelfAccess() {
+                return false;
+            }
+        };
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package1",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isFalse();
+        assertThat(VisibilityUtil.isSchemaSearchableByCaller(callerAccess,
+                /*targetPackageName=*/ "package2",
+                /*prefixedSchema=*/ "schema",
+                /*visibilityStore=*/ null,
+                /*visibilityChecker=*/ null)).isFalse();
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index c7cccb8..5cecaa7 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -48,6 +48,12 @@
                 // fall through
             case Features.LIST_FILTER_QUERY_LANGUAGE:
                 // fall through
+            case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+                // fall through
+            case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+                // fall through
+            case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+                // fall through
             case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
                 // fall through
             case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
@@ -60,15 +66,23 @@
                 // fall through
             case Features.SEARCH_SUGGESTION:
                 // fall through
-            case Features.SCHEMA_SET_DELETION_PROPAGATION:
-                // fall through
             case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
                 // fall through
             case Features.SCHEMA_ADD_PARENT_TYPE:
                 // fall through
+            case Features.SCHEMA_SET_DESCRIPTION:
+                // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
                 // fall through
             case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
+                // fall through
+            case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+                // fall through
+            case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
                 return true;
             default:
                 return false;
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 4c9fbfe..b8ea7099 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -16,7 +16,6 @@
 
 package androidx.appsearch.localstorage;
 
-import static androidx.appsearch.app.AppSearchResult.RESULT_INTERNAL_ERROR;
 import static androidx.appsearch.app.AppSearchResult.RESULT_SECURITY_ERROR;
 import static androidx.appsearch.app.InternalSetSchemaResponse.newFailedSetSchemaResponse;
 import static androidx.appsearch.app.InternalSetSchemaResponse.newSuccessfulSetSchemaResponse;
@@ -27,7 +26,6 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
-import android.os.Bundle;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -43,15 +41,16 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SearchResultPage;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SearchSuggestionResult;
 import androidx.appsearch.app.SearchSuggestionSpec;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
 import androidx.appsearch.localstorage.converter.ResultCodeToProtoConverter;
@@ -259,7 +258,7 @@
      * 

Instead, logger instance needs to be passed to each individual method, like create, query * and putDocument. * - * @param initStatsBuilder collects stats for initialization if provided. + * @param initStatsBuilder collects stats for initialization if provided. * @param visibilityChecker The {@link VisibilityChecker} that check whether the caller has * access to aa specific schema. Pass null will lost that ability and * global querier could only get their own data. @@ -269,8 +268,8 @@ @NonNull File icingDir, @NonNull AppSearchConfig config, @Nullable InitializeStats.Builder initStatsBuilder, - @NonNull OptimizeStrategy optimizeStrategy, - @Nullable VisibilityChecker visibilityChecker) + @Nullable VisibilityChecker visibilityChecker, + @NonNull OptimizeStrategy optimizeStrategy) throws AppSearchException { return new AppSearchImpl(icingDir, config, initStatsBuilder, optimizeStrategy, visibilityChecker); @@ -463,10 +462,10 @@ * @param packageName The package name that owns the schemas. * @param databaseName The name of the database where this schema lives. * @param schemas Schemas to set for this app. - * @param visibilityDocuments {@link VisibilityDocument}s that contain all + * @param visibilityConfigs {@link InternalVisibilityConfig}s that contain all * visibility setting information for those schemas * has user custom settings. Other schemas in the list - * that don't has a {@link VisibilityDocument} + * that don't has a {@link InternalVisibilityConfig} * will be treated as having the default visibility, * which is accessible by the system and no other packages. * @param forceOverride Whether to force-apply the schema even if it is @@ -490,7 +489,7 @@ @NonNull String packageName, @NonNull String databaseName, @NonNull List schemas, - @NonNull List visibilityDocuments, + @NonNull List visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException { @@ -508,7 +507,7 @@ packageName, databaseName, schemas, - visibilityDocuments, + visibilityConfigs, forceOverride, version, setSchemaStatsBuilder); @@ -517,7 +516,7 @@ packageName, databaseName, schemas, - visibilityDocuments, + visibilityConfigs, forceOverride, version, setSchemaStatsBuilder); @@ -539,7 +538,7 @@ @NonNull String packageName, @NonNull String databaseName, @NonNull List schemas, - @NonNull List visibilityDocuments, + @NonNull List visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException { @@ -588,7 +587,7 @@ packageName, databaseName, schemas, - visibilityDocuments, + visibilityConfigs, forceOverride, version, setSchemaStatsBuilder); @@ -717,7 +716,7 @@ @NonNull String packageName, @NonNull String databaseName, @NonNull List schemas, - @NonNull List visibilityDocuments, + @NonNull List visibilityConfigs, boolean forceOverride, int version, @Nullable SetSchemaStats.Builder setSchemaStatsBuilder) throws AppSearchException { @@ -801,32 +800,34 @@ // store before we have already created it. if (mVisibilityStoreLocked != null) { // Add prefix to all visibility documents. - List prefixedVisibilityDocuments = - new ArrayList<>(visibilityDocuments.size()); // Find out which Visibility document is deleted or changed to all-default settings. // We need to remove them from Visibility Store. Set deprecatedVisibilityDocuments = new ArraySet<>(rewrittenSchemaResults.mRewrittenPrefixedTypes.keySet()); - for (int i = 0; i < visibilityDocuments.size(); i++) { - VisibilityDocument unPrefixedDocument = visibilityDocuments.get(i); - // The VisibilityDocument is controlled by the client and it's untrusted but we + List prefixedVisibilityConfigs = + new ArrayList<>(visibilityConfigs.size()); + for (int i = 0; i < visibilityConfigs.size(); i++) { + InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); + // The VisibilityConfig is controlled by the client and it's untrusted but we // make it safe by appending a prefix. // We must control the package-database prefix. Therefore even if the client // fake the id, they can only mess their own app. That's totally allowed and // they can do this via the public API too. - String prefixedSchemaType = prefix + unPrefixedDocument.getId(); - prefixedVisibilityDocuments.add( - new VisibilityDocument.Builder( - unPrefixedDocument).setId(prefixedSchemaType).build()); + // TODO(b/275592563): Move prefixing into VisibilityConfig.createVisibilityDocument + // and createVisibilityOverlay + String schemaType = visibilityConfig.getSchemaType(); + String prefixedSchemaType = prefix + schemaType; + prefixedVisibilityConfigs.add(new InternalVisibilityConfig.Builder(visibilityConfig) + .setSchemaType(prefixedSchemaType).build()); // This schema has visibility settings. We should keep it from the removal list. - deprecatedVisibilityDocuments.remove(prefixedSchemaType); + deprecatedVisibilityDocuments.remove(visibilityConfig.getSchemaType()); } // Now deprecatedVisibilityDocuments contains those existing schemas that has // all-default visibility settings, add deleted schemas. That's all we need to // remove. deprecatedVisibilityDocuments.addAll(rewrittenSchemaResults.mDeletedPrefixedTypes); mVisibilityStoreLocked.removeVisibility(deprecatedVisibilityDocuments); - mVisibilityStoreLocked.setVisibility(prefixedVisibilityDocuments); + mVisibilityStoreLocked.setVisibility(prefixedVisibilityConfigs); } long saveVisibilitySettingEndTimeMillis = SystemClock.elapsedRealtime(); if (setSchemaStatsBuilder != null) { @@ -904,33 +905,44 @@ // schema. Avoid call visibility store before we have already created it. if (mVisibilityStoreLocked != null) { String typeName = typeConfig.getSchemaType().substring(typePrefix.length()); - VisibilityDocument visibilityDocument = + InternalVisibilityConfig visibilityConfig = mVisibilityStoreLocked.getVisibility(prefixedSchemaType); - if (visibilityDocument != null) { - if (visibilityDocument.isNotDisplayedBySystem()) { - responseBuilder - .addSchemaTypeNotDisplayedBySystem(typeName); + if (visibilityConfig != null) { + if (visibilityConfig.isNotDisplayedBySystem()) { + responseBuilder.addSchemaTypeNotDisplayedBySystem(typeName); } - String[] packageNames = visibilityDocument.getPackageNames(); - byte[][] sha256Certs = visibilityDocument.getSha256Certs(); - if (packageNames.length != sha256Certs.length) { - throw new AppSearchException(RESULT_INTERNAL_ERROR, - "The length of package names and sha256Crets are different!"); - } - if (packageNames.length != 0) { - Set packageIdentifier = new ArraySet<>(); - for (int j = 0; j < packageNames.length; j++) { - packageIdentifier.add(new PackageIdentifier( - packageNames[j], sha256Certs[j])); - } + List packageIdentifiers = + visibilityConfig.getVisibilityConfig().getAllowedPackages(); + if (!packageIdentifiers.isEmpty()) { responseBuilder.setSchemaTypeVisibleToPackages(typeName, - packageIdentifier); + new ArraySet<>(packageIdentifiers)); } Set> visibleToPermissions = - visibilityDocument.getVisibleToPermissions(); - if (visibleToPermissions != null) { - responseBuilder.setRequiredPermissionsForSchemaTypeVisibility( - typeName, visibleToPermissions); + visibilityConfig.getVisibilityConfig().getRequiredPermissions(); + if (!visibleToPermissions.isEmpty()) { + Set> visibleToPermissionsSet = + new ArraySet<>(visibleToPermissions.size()); + for (Set permissionList : visibleToPermissions) { + visibleToPermissionsSet.add(new ArraySet<>(permissionList)); + } + + responseBuilder.setRequiredPermissionsForSchemaTypeVisibility(typeName, + visibleToPermissionsSet); + } + + // Check for Visibility properties from the overlay + PackageIdentifier publiclyVisibleFromPackage = + visibilityConfig.getVisibilityConfig() + .getPubliclyVisibleTargetPackage(); + if (publiclyVisibleFromPackage != null) { + responseBuilder.setPubliclyVisibleSchema( + typeName, publiclyVisibleFromPackage); + } + Set visibleToConfigs = + visibilityConfig.getVisibleToConfigs(); + if (!visibleToConfigs.isEmpty()) { + responseBuilder.setSchemaTypeVisibleToConfigs( + typeName, visibleToConfigs); } } } @@ -1254,6 +1266,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, @@ -1317,8 +1331,9 @@ SearchStats.Builder sStatsBuilder = null; if (logger != null) { sStatsBuilder = - new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, - packageName).setDatabase(databaseName); + new SearchStats.Builder(SearchStats.VISIBILITY_SCOPE_LOCAL, packageName) + .setDatabase(databaseName) + .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()); } long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); @@ -1338,7 +1353,7 @@ if (sStatsBuilder != null && logger != null) { sStatsBuilder.setStatusCode(AppSearchResult.RESULT_SECURITY_ERROR); } - return new SearchResultPage(Bundle.EMPTY); + return new SearchResultPage(); } String prefix = createPrefix(packageName, databaseName); @@ -1349,7 +1364,7 @@ if (searchSpecToProtoConverter.hasNothingToSearch()) { // there is nothing to search over given their search filters, so we can return an // empty SearchResult and skip sending request to Icing. - return new SearchResultPage(Bundle.EMPTY); + return new SearchResultPage(); } SearchResultPage searchResultPage = @@ -1394,7 +1409,8 @@ sStatsBuilder = new SearchStats.Builder( SearchStats.VISIBILITY_SCOPE_GLOBAL, - callerAccess.getCallingPackageName()); + callerAccess.getCallingPackageName()) + .setSearchSourceLogTag(searchSpec.getSearchSourceLogTag()); } long javaLockAcquisitionLatencyStartMillis = SystemClock.elapsedRealtime(); @@ -1417,13 +1433,13 @@ // SearchSpec that wants to query every visible package. Set 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() + } else if (!joinSpec.getNestedSearchSpec() .getFilterPackageNames().isEmpty()) { packageFilters.addAll(searchSpec.getFilterPackageNames()); - packageFilters.addAll( - searchSpec.getJoinSpec().getNestedSearchSpec().getFilterPackageNames()); + packageFilters.addAll(joinSpec.getNestedSearchSpec().getFilterPackageNames()); } } @@ -1452,7 +1468,7 @@ if (searchSpecToProtoConverter.hasNothingToSearch()) { // there is nothing to search over given their search filters, so we can return an // empty SearchResult and skip sending request to Icing. - return new SearchResultPage(Bundle.EMPTY); + return new SearchResultPage(); } if (sStatsBuilder != null) { sStatsBuilder.setAclCheckLatencyMillis( @@ -1510,6 +1526,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, @@ -2758,22 +2776,26 @@ } if (LogUtil.DEBUG) { if (Log.isLoggable(icingTag, Log.VERBOSE)) { - IcingSearchEngine.setLoggingLevel(LogSeverity.Code.VERBOSE, /*verbosity=*/ - (short) 1); + boolean unused = IcingSearchEngine.setLoggingLevel(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)) { - IcingSearchEngine.setLoggingLevel(LogSeverity.Code.INFO); - } else if (Log.isLoggable(icingTag, Log.WARN)) { - IcingSearchEngine.setLoggingLevel(LogSeverity.Code.WARNING); + 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)) { - IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR); + boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.ERROR); } else { - IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL); + boolean unused = IcingSearchEngine.setLoggingLevel(LogSeverity.Code.FATAL); } } @@ -2814,7 +2836,7 @@ * {@link AppSearchResult} code. * @return {@link AppSearchResult} error code */ - private static @AppSearchResult.ResultCode int statusProtoToResultCode( + @AppSearchResult.ResultCode private static int statusProtoToResultCode( @NonNull StatusProto statusProto) { return ResultCodeToProtoConverter.toResultCode(statusProto.getCode()); }

diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
index 3e9e739..38afe59 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLogger.java
@@ -31,7 +31,7 @@
  * An interface for implementing client-defined logging AppSearch operations stats.
  *
  * 

Any implementation needs to provide general information on how to log all the stats types. - * (e.g. {@link CallStats}) + * (for example {@link CallStats}) * *

All implementations of this interface must be thread safe. *

diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
index 652fc94..74ed1c2 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchLoggerHelper.java
@@ -169,11 +169,9 @@
         Preconditions.checkNotNull(fromNativeStats);
         Preconditions.checkNotNull(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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
index 659433a..e040e50 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchMigrationHelper.java
@@ -19,7 +19,6 @@
 import static androidx.appsearch.app.AppSearchResult.RESULT_INVALID_SCHEMA;
 import static androidx.appsearch.app.AppSearchResult.throwableToFailedResult;
 
-import android.os.Bundle;
 import android.os.Parcel;
 
 import androidx.annotation.NonNull;
@@ -32,6 +31,7 @@
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
 import androidx.appsearch.stats.SchemaMigrationStats;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -130,11 +130,11 @@
                                         + newDocument.getSchemaType()
                                         + ". But the schema types doesn't exist in the request");
                     }
-                    Bundle bundle = newDocument.getBundle();
+                    GenericDocumentParcel documentParcel = newDocument.getDocumentParcel();
                     byte[] serializedMessage;
                     Parcel parcel = Parcel.obtain();
                     try {
-                        parcel.writeBundle(bundle);
+                        documentParcel.writeToParcel(parcel, /* flags= */ 0);
                         serializedMessage = parcel.marshall();
                     } finally {
                         parcel.recycle();
@@ -224,17 +224,17 @@
             @NonNull CodedInputStream codedInputStream) throws IOException {
         byte[] serializedMessage = codedInputStream.readByteArray();
 
-        Bundle bundle;
+        GenericDocumentParcel documentParcel;
         Parcel parcel = Parcel.obtain();
         try {
             parcel.unmarshall(serializedMessage, 0, serializedMessage.length);
             parcel.setDataPosition(0);
-            bundle = parcel.readBundle();
+            documentParcel = GenericDocumentParcel.CREATOR.createFromParcel(parcel);
         } finally {
             parcel.recycle();
         }
 
-        return new GenericDocument(bundle);
+        return new GenericDocument(documentParcel);
     }
 
     @Override
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
index ef8ec3b..7597399 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
@@ -93,7 +93,7 @@
             AppSearchBatchResult.Builder resultBuilder =
                     new AppSearchBatchResult.Builder<>();
 
-            Map> typePropertyPaths = request.getProjectionsInternal();
+            Map> typePropertyPaths = request.getProjections();
             CallerAccess access = new CallerAccess(mContext.getPackageName());
             for (String id : request.getIds()) {
                 try {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
index 870df5a..d3246bf 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/IcingOptionsConfig.java
@@ -36,7 +36,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
@@ -62,7 +62,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.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index db55481..bc9715b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -26,6 +26,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -39,7 +40,6 @@
 import java.io.File;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 /**
  * An AppSearch storage system which stores data locally in the app's storage space using a bundled
@@ -56,8 +56,6 @@
 public class LocalStorage {
     private static final String TAG = "AppSearchLocalStorage";
 
-    private static final String ICING_LIB_ROOT_DIR = "appsearch";
-
     /** Contains information about how to create the search session. */
     public static final class SearchContext {
         final Context mContext;
@@ -249,7 +247,8 @@
 
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .createCachedThreadPoolExecutor();
 
     private static volatile LocalStorage sInstance;
 
@@ -326,7 +325,8 @@
             @Nullable AppSearchLogger logger)
             throws AppSearchException {
         Preconditions.checkNotNull(context);
-        File icingDir = new File(context.getFilesDir(), ICING_LIB_ROOT_DIR);
+        File icingDir = AppSearchEnvironmentFactory.getEnvironmentInstance()
+                .getAppSearchDir(context, /* userHandle= */ null);
 
         long totalLatencyStartMillis = SystemClock.elapsedRealtime();
         InitializeStats.Builder initStatsBuilder = null;
@@ -346,8 +346,8 @@
                         /* shouldRetrieveParentInfo= */ true
                 ),
                 initStatsBuilder,
-                new JetpackOptimizeStrategy(),
-                /*visibilityChecker=*/null);
+                /*visibilityChecker=*/ null,
+                new JetpackOptimizeStrategy());
 
         if (logger != null) {
             initStatsBuilder.setTotalLatencyMillis(
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
index 74ea1c7..11b530e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorageIcingOptionsConfig.java
@@ -96,6 +96,6 @@
 
     @Override
     public boolean getBuildPropertyExistenceMetadataHits() {
-        return DEFAULT_BUILD_PROPERTY_EXISTENCE_METADATA_HITS;
+        return true;
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
index d7c46ef..907a078 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/ObserverManager.java
@@ -31,6 +31,7 @@
 import androidx.appsearch.observer.ObserverCallback;
 import androidx.appsearch.observer.ObserverSpec;
 import androidx.appsearch.observer.SchemaChangeInfo;
+import androidx.appsearch.util.ExceptionUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.ObjectsCompat;
@@ -75,8 +76,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)
@@ -410,8 +415,9 @@
 
                     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);
                     }
                 }
             }
@@ -429,8 +435,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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 36644b4..8094be0 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -34,6 +34,7 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
@@ -45,7 +46,6 @@
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.stats.OptimizeStats;
 import androidx.appsearch.localstorage.stats.RemoveStats;
@@ -80,7 +80,6 @@
     private final AppSearchImpl mAppSearchImpl;
     private final Executor mExecutor;
     private final Features mFeatures;
-    private final Context mContext;
     private final String mDatabaseName;
     @Nullable private final AppSearchLogger mLogger;
 
@@ -100,11 +99,11 @@
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
         mExecutor = Preconditions.checkNotNull(executor);
         mFeatures = Preconditions.checkNotNull(features);
-        mContext = Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(context);
         mDatabaseName = Preconditions.checkNotNull(databaseName);
         mLogger = logger;
 
-        mPackageName = mContext.getPackageName();
+        mPackageName = context.getPackageName();
         mSelfCallerAccess = new CallerAccess(/*callingPackageName=*/mPackageName);
     }
 
@@ -126,15 +125,15 @@
                         mPackageName, mDatabaseName);
             }
 
-            // Extract a Map from the request.
-            List visibilityDocuments = VisibilityDocument
-                    .toVisibilityDocuments(request);
+            List visibilityConfigs =
+                    InternalVisibilityConfig.toInternalVisibilityConfigs(request);
 
             Map migrators = request.getMigrators();
             // No need to trigger migration if user never set migrator.
-            if (migrators.size() == 0) {
+            if (migrators.isEmpty()) {
                 SetSchemaResponse setSchemaResponse = setSchemaNoMigrations(request,
-                        visibilityDocuments, firstSetSchemaStatsBuilder);
+                        visibilityConfigs,
+                        firstSetSchemaStatsBuilder);
 
                 long dispatchNotificationStartTimeMillis = SystemClock.elapsedRealtime();
                 // Schedule a task to dispatch change notifications. See requirements for where the
@@ -172,9 +171,9 @@
             Map activeMigrators = SchemaMigrationUtil.getActiveMigrators(
                     getSchemaResponse.getSchemas(), migrators, currentVersion, finalVersion);
             // No need to trigger migration if no migrator is active.
-            if (activeMigrators.size() == 0) {
+            if (activeMigrators.isEmpty()) {
                 SetSchemaResponse setSchemaResponse = setSchemaNoMigrations(request,
-                        visibilityDocuments, firstSetSchemaStatsBuilder);
+                        visibilityConfigs, firstSetSchemaStatsBuilder);
                 if (firstSetSchemaStatsBuilder != null) {
                     firstSetSchemaStatsBuilder.setTotalLatencyMillis(
                             (int) (SystemClock.elapsedRealtime() - startMillis));
@@ -194,7 +193,7 @@
                     mPackageName,
                     mDatabaseName,
                     new ArrayList<>(request.getSchemas()),
-                    visibilityDocuments,
+                    visibilityConfigs,
                     /*forceOverride=*/false,
                     request.getVersion(),
                     firstSetSchemaStatsBuilder);
@@ -233,7 +232,7 @@
                             mPackageName,
                             mDatabaseName,
                             new ArrayList<>(request.getSchemas()),
-                            visibilityDocuments,
+                            visibilityConfigs,
                             /*forceOverride=*/ true,
                             request.getVersion(),
                             secondSetSchemaStatsBuilder);
@@ -246,9 +245,8 @@
                     }
                 }
                 long secondSetSchemaLatencyEndTimeMillis = SystemClock.elapsedRealtime();
-                SetSchemaResponse.Builder responseBuilder = internalSetSchemaResponse
-                        .getSetSchemaResponse()
-                        .toBuilder()
+                SetSchemaResponse.Builder responseBuilder = new SetSchemaResponse.Builder(
+                        internalSetSchemaResponse.getSetSchemaResponse())
                         .addMigratedTypes(activeMigrators.keySet());
                 mIsMutated = true;
 
@@ -352,23 +350,26 @@
             @NonNull PutDocumentsRequest request) {
         Preconditions.checkNotNull(request);
         Preconditions.checkState(!mIsClosed, "AppSearchSession has already been closed");
+
+        List documents = request.getGenericDocuments();
+        List takenActions = request.getTakenActionGenericDocuments();
+
         ListenableFuture> future = execute(() -> {
             AppSearchBatchResult.Builder resultBuilder =
                     new AppSearchBatchResult.Builder<>();
-            for (int i = 0; i < request.getGenericDocuments().size(); i++) {
-                GenericDocument document = request.getGenericDocuments().get(i);
-                try {
-                    mAppSearchImpl.putDocument(
-                            mPackageName,
-                            mDatabaseName,
-                            document,
-                            /*sendChangeNotifications=*/ true,
-                            mLogger);
-                    resultBuilder.setSuccess(document.getId(), /*value=*/ null);
-                } catch (Throwable t) {
-                    resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
-                }
+
+            // Normal documents.
+            for (int i = 0; i < documents.size(); i++) {
+                GenericDocument document = documents.get(i);
+                putGenericDocument(document, resultBuilder);
             }
+
+            // TakenAction documents.
+            for (int i = 0; i < takenActions.size(); i++) {
+                GenericDocument takenActionGenericDocument = takenActions.get(i);
+                putGenericDocument(takenActionGenericDocument, resultBuilder);
+            }
+
             // Now that the batch has been written. Persist the newly written data.
             mAppSearchImpl.persistToDisk(PersistType.Code.LITE);
             mIsMutated = true;
@@ -382,7 +383,7 @@
 
         // The existing documents with same ID will be deleted, so there may be some resources that
         // could be released after optimize().
-        checkForOptimize(/*mutateBatchSize=*/ request.getGenericDocuments().size());
+        checkForOptimize(/*mutateBatchSize=*/ documents.size() + takenActions.size());
         return future;
     }
 
@@ -396,7 +397,7 @@
             AppSearchBatchResult.Builder resultBuilder =
                     new AppSearchBatchResult.Builder<>();
 
-            Map> typePropertyPaths = request.getProjectionsInternal();
+            Map> typePropertyPaths = request.getProjections();
             for (String id : request.getIds()) {
                 try {
                     GenericDocument document =
@@ -583,7 +584,7 @@
      * forceoverride in the request.
      */
     private SetSchemaResponse setSchemaNoMigrations(@NonNull SetSchemaRequest request,
-            @NonNull List visibilityDocuments,
+            @NonNull List visibilityConfigs,
             @Nullable SetSchemaStats.Builder setSchemaStatsBuilder)
             throws AppSearchException {
         if (setSchemaStatsBuilder != null) {
@@ -593,7 +594,7 @@
                 mPackageName,
                 mDatabaseName,
                 new ArrayList<>(request.getSchemas()),
-                visibilityDocuments,
+                visibilityConfigs,
                 request.isForceOverride(),
                 request.getVersion(),
                 setSchemaStatsBuilder);
@@ -621,6 +622,28 @@
         mAppSearchImpl.dispatchAndClearChangeNotifications();
     }
 
+    /**
+     * Calls {@link AppSearchImpl} to put a generic document and sets the result.
+     *
+     * @param document the {@link GenericDocument} to put.
+     * @param resultBuilder an {@link AppSearchBatchResult.Builder} object for collecting the
+     *                      result.
+     */
+    private void putGenericDocument(
+            GenericDocument document, AppSearchBatchResult.Builder resultBuilder) {
+        try {
+            mAppSearchImpl.putDocument(
+                    mPackageName,
+                    mDatabaseName,
+                    document,
+                    /*sendChangeNotifications=*/ true,
+                    mLogger);
+            resultBuilder.setSuccess(document.getId(), /*value=*/ null);
+        } catch (Throwable t) {
+            resultBuilder.setResult(document.getId(), throwableToFailedResult(t));
+        }
+    }
+
     private void checkForOptimize(int mutateBatchSize) {
         mExecutor.execute(() -> {
             long totalLatencyStartMillis = SystemClock.elapsedRealtime();
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index aedd191..2320d8b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchConfig;
@@ -54,6 +55,8 @@
     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() {
     }
@@ -109,6 +112,12 @@
                     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 +214,13 @@
                             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 =
@@ -216,6 +232,37 @@
     }
 
     /**
+     * Converts a {@link PropertyProto.VectorProto} into an {@link EmbeddingVector}.
+     */
+    @NonNull
+    public static EmbeddingVector vectorProtoToEmbeddingVector(
+            @NonNull PropertyProto.VectorProto vectorProto) {
+        Preconditions.checkNotNull(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) {
+        Preconditions.checkNotNull(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}.
      *
      * 

It's guaranteed that child types always appear before parent types in the list. @@ -305,6 +352,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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
index 2106917..eedbb0d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/ResultCodeToProtoConverter.java
@@ -35,7 +35,7 @@
     private ResultCodeToProtoConverter() {}
 
     /** Converts an {@link StatusProto.Code} into a {@link AppSearchResult.ResultCode}. */
-    public static @AppSearchResult.ResultCode int toResultCode(
+    @AppSearchResult.ResultCode public static int toResultCode(
             @NonNull StatusProto.Code statusCode) {
         switch (statusCode) {
             case OK:
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index e21213f..7778eb6 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -24,6 +24,7 @@
 import androidx.core.util.Preconditions;
 
 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;
@@ -55,6 +56,7 @@
         Preconditions.checkNotNull(schema);
         SchemaTypeConfigProto.Builder protoBuilder = SchemaTypeConfigProto.newBuilder()
                 .setSchemaType(schema.getSchemaType())
+                .setDescription(schema.getDescription())
                 .setVersion(version);
         List properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
@@ -70,7 +72,8 @@
             @NonNull AppSearchSchema.PropertyConfig property) {
         Preconditions.checkNotNull(property);
         PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
-                .setPropertyName(property.getName());
+                .setPropertyName(property.getName())
+                .setDescription(property.getDescription());
 
         // Set dataType
         @AppSearchSchema.PropertyConfig.DataType int dataType = property.getDataType();
@@ -101,11 +104,6 @@
                         .setValueType(
                                 convertJoinableValueTypeToProto(
                                         stringProperty.getJoinableValueType()))
-                        // @exportToFramework:startStrip()
-                        // Do not call this in framework as it will populate the proto field and
-                        // fail comparison tests.
-                        .setPropagateDelete(stringProperty.getDeletionPropagation())
-                        // @exportToFramework:endStrip()
                         .build();
                 builder.setJoinableConfig(joinableConfig);
             }
@@ -140,6 +138,20 @@
                         .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();
     }
@@ -154,6 +166,7 @@
         Preconditions.checkNotNull(proto);
         AppSearchSchema.Builder builder =
                 new AppSearchSchema.Builder(proto.getSchemaType());
+        builder.setDescription(proto.getDescription());
         List properties = proto.getPropertiesList();
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
@@ -177,20 +190,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());
         }
     }
 
@@ -199,11 +218,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());
 
@@ -220,6 +239,7 @@
         AppSearchSchema.DocumentPropertyConfig.Builder builder =
                 new AppSearchSchema.DocumentPropertyConfig.Builder(
                                 proto.getPropertyName(), proto.getSchemaType())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber())
                         .setShouldIndexNestedProperties(
                                 proto.getDocumentIndexingConfig().getIndexNestedProperties());
@@ -233,6 +253,7 @@
             @NonNull PropertyConfigProto proto) {
         AppSearchSchema.LongPropertyConfig.Builder builder =
                 new AppSearchSchema.LongPropertyConfig.Builder(proto.getPropertyName())
+                        .setDescription(proto.getDescription())
                         .setCardinality(proto.getCardinality().getNumber());
 
         // Set indexingType
@@ -244,6 +265,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) {
@@ -265,12 +301,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
@@ -297,12 +332,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
@@ -337,11 +371,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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index 8652cca..2396f9d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -20,8 +20,6 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
-import android.os.Bundle;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchResult;
@@ -39,6 +37,7 @@
 import com.google.android.icing.proto.SnippetProto;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -64,15 +63,12 @@
             @NonNull Map> schemaMap,
             @NonNull AppSearchConfig config)
             throws AppSearchException {
-        Bundle bundle = new Bundle();
-        bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
-        ArrayList resultBundles = new ArrayList<>(proto.getResultsCount());
+        List results = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
             SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap, config);
-            resultBundles.add(result.getBundle());
+            results.add(result);
         }
-        bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
-        return new SearchResultPage(bundle);
+        return new SearchResultPage(proto.getNextPageToken(), results);
     }
 
     /**
@@ -100,6 +96,9 @@
         SearchResult.Builder builder =
                 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);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index 42cfd44..b81b91d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -26,6 +26,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.FeatureConstants;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchSpec;
@@ -190,12 +192,12 @@
     }
 
     /**
-     * @return whether this search's target filters are empty. If any target filter is empty, we
+     * Returns whether this search's target filters are empty. If any target filter is empty, we
      * should skip send request to Icing.
      *
-     * 

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. + *

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(); @@ -291,6 +293,13 @@ .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters) .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch()); + List 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> entry : mSearchSpec.getFilterProperties().entrySet()) { @@ -319,6 +328,17 @@ } 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() @@ -342,18 +362,18 @@ protoBuilder.setJoinSpec(joinSpecProtoBuilder); } - // TODO(b/208654892) Remove this field once EXPERIMENTAL_ICING_ADVANCED_QUERY is fully - // supported. - boolean turnOnIcingAdvancedQuery = - mSearchSpec.isNumericSearchEnabled() || mSearchSpec.isVerbatimSearchEnabled() - || mSearchSpec.isListFilterQueryLanguageEnabled(); - if (turnOnIcingAdvancedQuery) { - protoBuilder.setSearchType( - SearchSpecProto.SearchType.Code.EXPERIMENTAL_ICING_ADVANCED_QUERY); + if (mSearchSpec.isListFilterHasPropertyFunctionEnabled() + && !mIcingOptionsConfig.getBuildPropertyExistenceMetadataHits()) { + // This condition should never be reached as long as Features.isFeatureSupported() is + // consistent with IcingOptionsConfig. + throw new UnsupportedOperationException( + FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION + + " is currently not operational because the building process for the " + + "associated metadata has not yet been turned on."); } // Set enabled features - protoBuilder.addAllEnabledFeatures(mSearchSpec.getEnabledFeatures()); + protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures(mSearchSpec.getEnabledFeatures())); return protoBuilder.build(); } @@ -468,7 +488,7 @@ // Rewrite filters to include a database prefix. for (int i = 0; i < typePropertyMaskBuilders.size(); i++) { String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType(); - if (unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD)) { + if (unprefixedType.equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) { resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i).build()); } else { // Qualify the given schema types @@ -502,6 +522,8 @@ addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder); protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression()); + protoBuilder.addAllAdditionalAdvancedScoringExpressions( + mSearchSpec.getInformationalRankingExpressions()); return protoBuilder.build(); } @@ -536,6 +558,26 @@ } /** + * Maps a list of AppSearch search feature strings to the list of the corresponding Icing + * feature strings. + * + * @param appSearchFeatures The list of AppSearch search feature strings. + */ + @NonNull + private static List toIcingSearchFeatures(@NonNull List appSearchFeatures) { + List result = new ArrayList<>(); + for (int i = 0; i < appSearchFeatures.size(); i++) { + String appSearchFeature = appSearchFeatures.get(i); + if (appSearchFeature.equals(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION)) { + result.add("HAS_PROPERTY_FUNCTION"); + } else { + result.add(appSearchFeature); + } + } + return result; + } + + /** * Returns a Map of namespace to prefixedNamespaces. This is NOT necessarily the * same as the list of namespaces. If a namespace exists under different packages and/or * different databases, they should still be grouped together. @@ -593,7 +635,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 {

diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
index 52f57bd..416bdce 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
@@ -87,7 +87,7 @@
     }
 
     /**
-     * @return whether this search's target filters are empty. If any target filter is empty, we
+     * 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() {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
index 02d2971..5f28ea0 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/CallStats.java
@@ -45,6 +45,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class CallStats {
+    /** Call types. */
     @IntDef(value = {
             CALL_TYPE_UNKNOWN,
             CALL_TYPE_INITIALIZE,
@@ -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 {
@@ -113,6 +115,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";
@@ -144,6 +147,7 @@
     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;
@@ -410,6 +414,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;
         }
@@ -445,6 +451,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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java
new file mode 100644
index 0000000..7419e6b
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/ClickStats.java
@@ -0,0 +1,117 @@
+/*
+ * 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.core.util.Preconditions;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a click action, converted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ClickStats {
+    private final long mTimestampMillis;
+
+    private final long mTimeStayOnResultMillis;
+
+    private final int mResultRankInBlock;
+
+    private final int mResultRankGlobal;
+
+    ClickStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(builder);
+        mTimestampMillis = builder.mTimestampMillis;
+        mTimeStayOnResultMillis = builder.mTimeStayOnResultMillis;
+        mResultRankInBlock = builder.mResultRankInBlock;
+        mResultRankGlobal = builder.mResultRankGlobal;
+    }
+
+    /** 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;
+    }
+
+    /** Builder for {@link ClickStats} */
+    public static final class Builder {
+        private long mTimestampMillis;
+
+        private long mTimeStayOnResultMillis;
+
+        private int mResultRankInBlock;
+
+        private int mResultRankGlobal;
+
+        /** 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;
+        }
+
+        /** Builds a new {@link ClickStats} from the {@link ClickStats.Builder}. */
+        @NonNull
+        public ClickStats build() {
+            return new ClickStats(/* builder= */ this);
+        }
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
index 73a8c96..d650e7d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/RemoveStats.java
@@ -37,6 +37,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class RemoveStats {
+    /** Types of stats available for remove API. */
     @IntDef(value = {
             // It needs to be sync with DeleteType.Code in
             // external/icing/proto/icing/proto/logging.proto#DeleteStatsProto
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java
new file mode 100644
index 0000000..f850b63
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchIntentStats.java
@@ -0,0 +1,279 @@
+/*
+ * 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 androidx.appsearch.localstorage.stats;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.core.util.Preconditions;
+
+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;
+
+// TODO(b/319285816): link converter here.
+/**
+ * Class holds detailed stats of a search intent, converted from
+ * {@link androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * 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 androidx.appsearch.app.PutDocumentsRequest#getTakenActionGenericDocuments}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+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,
+    })
+    @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;
+
+    @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 mClicksStats;
+
+    SearchIntentStats(@NonNull Builder builder) {
+        Preconditions.checkNotNull(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. */
+    @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 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 mClicksStats = new ArrayList<>();
+
+        private boolean mBuilt = false;
+
+        /** Constructor for the {@link Builder}. */
+        public Builder(@NonNull String packageName) {
+            mPackageName = Preconditions.checkNotNull(packageName);
+        }
+
+        /** Sets calling database name. */
+        @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
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder addClicksStats(@NonNull ClickStats... clicksStats) {
+            Preconditions.checkNotNull(clicksStats);
+            resetIfBuilt();
+            return addClicksStats(Arrays.asList(clicksStats));
+        }
+
+        /** Adds a collection of {@link ClickStats} objects to this search intent. */
+        @CanIgnoreReturnValue
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder addClicksStats(@NonNull Collection clicksStats) {
+            Preconditions.checkNotNull(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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
index a87ae04..28d5117 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
@@ -36,6 +36,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SearchStats {
+    /** Types of Visibility scopes available for search. */
     @IntDef(value = {
             // Searches apps' own documents.
             VISIBILITY_SCOPE_LOCAL,
@@ -137,6 +138,7 @@
     private final int mNativeNumJoinedResultsCurrentPage;
     /** Time taken to join documents together. */
     private final int mNativeJoinLatencyMillis;
+    @Nullable private final String mSearchSourceLogTag;
 
     SearchStats(@NonNull Builder builder) {
         Preconditions.checkNotNull(builder);
@@ -170,6 +172,7 @@
         mJoinType = builder.mJoinType;
         mNativeNumJoinedResultsCurrentPage = builder.mNativeNumJoinedResultsCurrentPage;
         mNativeJoinLatencyMillis = builder.mNativeJoinLatencyMillis;
+        mSearchSourceLogTag = builder.mSearchSourceLogTag;
     }
 
     /** Returns the package name of the session. */
@@ -342,6 +345,12 @@
         return mNativeJoinLatencyMillis;
     }
 
+    /**  Returns a tag to indicate the source of this search, or {code null} if never set. */
+    @Nullable
+    public String getSearchSourceLogTag() {
+        return mSearchSourceLogTag;
+    }
+
     /** Builder for {@link SearchStats} */
     public static class Builder {
         @NonNull
@@ -377,6 +386,7 @@
         @JoinableValueType int mJoinType;
         int mNativeNumJoinedResultsCurrentPage;
         int mNativeJoinLatencyMillis;
+        @Nullable String mSearchSourceLogTag;
 
         /**
          * Constructor
@@ -604,6 +614,7 @@
         }
 
         /** Sets whether or not this is a join query */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setJoinType(@JoinableValueType int joinType) {
             mJoinType = joinType;
@@ -611,6 +622,7 @@
         }
 
         /** Set the total number of joined documents in a page. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setNativeNumJoinedResultsCurrentPage(int nativeNumJoinedResultsCurrentPage) {
             mNativeNumJoinedResultsCurrentPage = nativeNumJoinedResultsCurrentPage;
@@ -618,12 +630,21 @@
         }
 
         /** Sets time it takes to join documents together in icing. */
+        @CanIgnoreReturnValue
         @NonNull
         public Builder setNativeJoinLatencyMillis(int nativeJoinLatencyMillis) {
             mNativeJoinLatencyMillis = nativeJoinLatencyMillis;
             return this;
         }
 
+        /** Sets a tag to indicate the source of this search. */
+        @CanIgnoreReturnValue
+        @NonNull
+        public Builder setSearchSourceLogTag(@Nullable String searchSourceLogTag) {
+            mSearchSourceLogTag = searchSourceLogTag;
+            return this;
+        }
+
         /**
          * Constructs a new {@link SearchStats} from the contents of this
          * {@link SearchStats.Builder}.
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java
new file mode 100644
index 0000000..baa3e4e
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/ClickActionGenericDocument.java
@@ -0,0 +1,174 @@
+/*
+ * 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 androidx.appsearch.localstorage.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Wrapper class for
+ *  
+ *  {@link androidx.appsearch.usagereporting.ClickAction}
+ *  
+ * {@link GenericDocument}, which contains getters for click action properties.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+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(Preconditions.checkNotNull(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 {
+        /**
+         * Creates a new {@link ClickActionGenericDocument.Builder}.
+         *
+         * 

Document IDs are unique within a namespace. + * + *

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#setSchemaAsync} prior + * to inserting a document of this {@code schemaType} into the + * AppSearch index using + * {@link AppSearchSession#putAsync}. + * Otherwise, the document will be rejected by + * {@link AppSearchSession#putAsync} with result code + * {@link AppSearchResult#RESULT_NOT_FOUND}. + */ + public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) { + super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id), + Preconditions.checkNotNull(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(Preconditions.checkNotNull(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 androidx.appsearch.app.SearchResult} on which the user clicked. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setQuery(@NonNull String query) { + Preconditions.checkNotNull(query); + setPropertyString(PROPERTY_PATH_QUERY, query); + return this; + } + + /** + * Sets the integer value of property {@code resultRankInBlock} by the rank of the clicked + * {@link androidx.appsearch.app.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 androidx.appsearch.app.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 androidx.appsearch.app.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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java
new file mode 100644
index 0000000..bedbf21
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchActionGenericDocument.java
@@ -0,0 +1,137 @@
+/*
+ * 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 androidx.appsearch.localstorage.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Wrapper class for
+ *  
+ *  {@link androidx.appsearch.usagereporting.SearchAction}
+ *  
+ * {@link GenericDocument}, which contains getters for search action properties.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+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(Preconditions.checkNotNull(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 {
+        /**
+         * Creates a new {@link SearchActionGenericDocument.Builder}.
+         *
+         * 

Document IDs are unique within a namespace. + * + *

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#setSchemaAsync} prior + * to inserting a document of this {@code schemaType} into the + * AppSearch index using + * {@link AppSearchSession#putAsync}. + * Otherwise, the document will be rejected by + * {@link AppSearchSession#putAsync} with result code + * {@link AppSearchResult#RESULT_NOT_FOUND}. + */ + public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) { + super(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id), + Preconditions.checkNotNull(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(Preconditions.checkNotNull(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) { + Preconditions.checkNotNull(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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchIntentStatsExtractor.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchIntentStatsExtractor.java
new file mode 100644
index 0000000..0d0690e
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/SearchIntentStatsExtractor.java
@@ -0,0 +1,337 @@
+/*
+ * 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 androidx.appsearch.localstorage.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.localstorage.stats.ClickStats;
+import androidx.appsearch.localstorage.stats.SearchIntentStats;
+import androidx.appsearch.usagereporting.ActionConstants;
+
+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 SearchIntentStats}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SearchIntentStatsExtractor {
+    // 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:
+     * 
    + *
  • The action timestamp (action document creation timestamp) difference between it and + * its previous search action is below this threshold. + *
  • There is no click action associated with it. + *
  • Its raw query string is a prefix of the previous search action's raw query string (or + * the other way around). + *
+ */ + 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, 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 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; + + @NonNull + private final String mPackageName; + @Nullable + private final String mDatabase; + + /** + * Constructs {@link SearchIntentStatsExtractor} with the caller's package and database name. + * + * @param packageName The package name of the caller. + * @param database The database name of the caller. + */ + public SearchIntentStatsExtractor(@NonNull String packageName, @Nullable String database) { + Objects.requireNonNull(packageName); + mPackageName = packageName; + mDatabase = database; + } + + /** + * 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 SearchIntentStats} extracted from the given list of taken action + * {@link GenericDocument} . + * + *

Search intent is consist 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. + * + * @param genericDocuments a list of taken actions in generic document form. + */ + @NonNull + public List extract( + @NonNull List genericDocuments) { + Objects.requireNonNull(genericDocuments); + + // Convert GenericDocument list to TakenActionGenericDocument list and sort them by document + // creation timestamp. + List 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 result = new ArrayList<>(); + 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. + 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 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 (isIndependentSearchAction(currSearchAction, prevSearchAction)) { + // If the current search action is independent from the previous one, then ignore + // the previous search action when extracting stats. + 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. + result.add(createSearchIntentStats(currSearchAction, clickActions, prevSearchAction)); + prevSearchAction = currSearchAction; + } + 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 SearchActionGenericDocument currSearchAction, + @NonNull List clickActions, + @Nullable SearchActionGenericDocument prevSearchAction) { + SearchIntentStats.Builder builder = new SearchIntentStats.Builder(mPackageName) + .setDatabase(mDatabase) + .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) { + return new ClickStats.Builder() + .setTimestampMillis(clickAction.getCreationTimestampMillis()) + .setResultRankInBlock(clickAction.getResultRankInBlock()) + .setResultRankGlobal(clickAction.getResultRankGlobal()) + .setTimeStayOnResultMillis(clickAction.getTimeStayOnResultMillis()) + .build(); + } + + /** + * Returns if the current search action is an intermediate search action. + * + *

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: + *

    + *
  • There are related (non-independent) search actions before and after it. + *
  • It occurs within the threshold after its previous search action. + *
  • Its raw query string is a prefix of its previous search action's raw query string, or + * the opposite direction. + *
+ */ + 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 related search sequence. + 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. + * + *

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, + @Nullable SearchActionGenericDocument prevSearchAction) { + Objects.requireNonNull(currSearchAction); + + if (prevSearchAction == null) { + return true; + } + + 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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/usagereporting/TakenActionGenericDocument.java
new file mode 100644
index 0000000..1af156d
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/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 androidx.appsearch.localstorage.usagereporting;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.core.util.Preconditions;
+
+/**
+ * Abstract wrapper class for {@link GenericDocument} of all types of taken actions, which contains
+ * common getters and constants.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+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 {
+        Preconditions.checkNotNull(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(Preconditions.checkNotNull(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> extends GenericDocument.Builder {
+        /**
+         * Creates a new {@link TakenActionGenericDocument.Builder}.
+         *
+         * 

Document IDs are unique within a namespace. + * + *

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#setSchemaAsync} prior + * to inserting a document of this {@code schemaType} into the + * AppSearch index using + * {@link AppSearchSession#putAsync}. + * Otherwise, the document will be rejected by + * {@link AppSearchSession#putAsync} 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(Preconditions.checkNotNull(namespace), Preconditions.checkNotNull(id), + Preconditions.checkNotNull(schemaType)); + + setPropertyLong(PROPERTY_PATH_ACTION_TYPE, actionType); + } + + /** + * Creates a new {@link TakenActionGenericDocument.Builder} from an existing + * {@link GenericDocument}. + */ + Builder(@NonNull GenericDocument document) { + super(Preconditions.checkNotNull(document)); + } + } +}

diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
index a22863a..4deb5c3 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/CallerAccess.java
@@ -44,10 +44,19 @@
         return mCallingPackageName;
     }
 
+    /** Returns whether the caller should have default access to data in its own package. */
+    public boolean doesCallerHaveSelfAccess() {
+        return true;
+    }
+
     @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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
index 9db52a5..13f908d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityChecker.java
@@ -38,4 +38,11 @@
             @NonNull String packageName,
             @NonNull String prefixedSchema,
             @NonNull VisibilityStore visibilityStore);
+
+    /**
+     * Checks whether the given package has access to system-surfaceable schemas.
+     *
+     * @param callerPackageName Package name of the caller.
+     */
+    boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName);
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
index a45cf75..7bcea68 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityDocumentV1.java
@@ -18,6 +18,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.PackageIdentifier;
@@ -143,6 +144,7 @@
         }
 
         /** Sets whether this schema has opted out of platform surfacing. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) {
             return setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY,
@@ -150,6 +152,7 @@
         }
 
         /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder addVisibleToPackages(@NonNull Set packageIdentifiers) {
             Preconditions.checkNotNull(packageIdentifiers);
@@ -158,6 +161,7 @@
         }
 
         /** Add {@link PackageIdentifier} of packages which has access to this schema. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) {
             Preconditions.checkNotNull(packageIdentifier);
@@ -167,6 +171,7 @@
 
         /** Add a set of Android role that has access to the schema this
          * {@link VisibilityDocumentV1} represents. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder setVisibleToRoles(@NonNull Set visibleToRoles) {
             Preconditions.checkNotNull(visibleToRoles);
@@ -176,6 +181,7 @@
 
         /** Add a set of Android role that has access to the schema this
          * {@link VisibilityDocumentV1} represents. */
+        @CanIgnoreReturnValue
         @NonNull
         Builder setVisibleToPermissions(@NonNull Set visibleToPermissions) {
             Preconditions.checkNotNull(visibleToPermissions);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
index e3d5916..c221673 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStore.java
@@ -27,11 +27,15 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.InternalSetSchemaResponse;
-import androidx.appsearch.app.VisibilityDocument;
-import androidx.appsearch.app.VisibilityPermissionDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.appsearch.checker.initialization.qual.UnderInitialization;
+import androidx.appsearch.checker.initialization.qual.UnknownInitialization;
+import androidx.appsearch.checker.nullness.qual.RequiresNonNull;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.util.LogUtil;
 import androidx.collection.ArrayMap;
 import androidx.core.util.Preconditions;
 
@@ -47,10 +51,11 @@
  * Stores all visibility settings for all databases that AppSearchImpl knows about.
  * Persists the visibility settings and reloads them on initialization.
  *
- * 

The VisibilityStore creates a {@link VisibilityDocument} for each schema. This document holds - * the visibility settings that apply to that schema. The VisibilityStore also creates a - * schema for these documents and has its own package and database so that its data doesn't - * interfere with any clients' data. It persists the document and schema through AppSearchImpl. + *

The VisibilityStore creates a {@link InternalVisibilityConfig} for each schema. This config + * holds the visibility settings that apply to that schema. The VisibilityStore also creates a + * schema and documents for these {@link InternalVisibilityConfig} and has its own + * package and database so that its data doesn't interfere with any clients' data. It persists + * the document and schema through AppSearchImpl. * *

These visibility settings won't be used in AppSearch Jetpack, we only store them for clients * to look up. @@ -67,12 +72,13 @@ public static final String VISIBILITY_PACKAGE_NAME = "VS#Pkg"; public static final String VISIBILITY_DATABASE_NAME = "VS#Db"; + public static final String ANDROID_V_OVERLAY_DATABASE_NAME = "VS#AndroidVDb"; /** - * Map of PrefixedSchemaType and VisibilityDocument stores visibility information for each + * Map of PrefixedSchemaType to InternalVisibilityConfig stores visibility information for each * schema type. */ - private final Map mVisibilityDocumentMap = new ArrayMap<>(); + private final Map mVisibilityConfigMap = new ArrayMap<>(); private final AppSearchImpl mAppSearchImpl; @@ -86,7 +92,7 @@ new CallerAccess(/*callingPackageName=*/VISIBILITY_PACKAGE_NAME)); List visibilityDocumentsV1s = null; switch (getSchemaResponse.getVersion()) { - case VisibilityDocument.SCHEMA_VERSION_DOC_PER_PACKAGE: + case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_PACKAGE: // TODO (b/202194495) add VisibilityDocument in version 0 back instead of using // GenericDocument. List visibilityDocumentsV0s = @@ -95,7 +101,7 @@ visibilityDocumentsV1s = VisibilityStoreMigrationHelperFromV0 .toVisibilityDocumentV1(visibilityDocumentsV0s); // fall through - case VisibilityDocument.SCHEMA_VERSION_DOC_PER_SCHEMA: + case VisibilityToDocumentConverter.SCHEMA_VERSION_DOC_PER_SCHEMA: if (visibilityDocumentsV1s == null) { // We need to read VisibilityDocument in Version 1 from AppSearch instead of // taking from the above step. @@ -106,37 +112,12 @@ setLatestSchemaAndDocuments(VisibilityStoreMigrationHelperFromV1 .toVisibilityDocumentsV2(visibilityDocumentsV1s)); break; - case VisibilityDocument.SCHEMA_VERSION_LATEST: - Set existingVisibilitySchema = getSchemaResponse.getSchemas(); - if (existingVisibilitySchema.contains(VisibilityDocument.SCHEMA) - && existingVisibilitySchema.contains(VisibilityPermissionDocument.SCHEMA)) { - // The latest Visibility schema is in AppSearch, we must find our schema type. - // Extract all stored Visibility Document into mVisibilityDocumentMap. - loadVisibilityDocumentMap(); - } else { - // We must have a broken schema. Reset it to the latest version. - // Do NOT set forceOverride to be true here, see comment below. - InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( - VISIBILITY_PACKAGE_NAME, - VISIBILITY_DATABASE_NAME, - Arrays.asList(VisibilityDocument.SCHEMA, - VisibilityPermissionDocument.SCHEMA), - /*visibilityDocuments=*/ Collections.emptyList(), - /*forceOverride=*/ false, - /*version=*/ VisibilityDocument.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 - // the version number and create a VisibilityStoreMigrationHelper which - // can analyse the different between the old version and the new version - // to migration user's visibility settings. - throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, - "Fail to set the latest visibility schema to AppSearch. " - + "You may need to update the visibility schema version " - + "number."); - } - } + case VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST: + verifyOrSetLatestVisibilitySchema(getSchemaResponse); + // Check the version for visibility overlay database. + migrateVisibilityOverlayDatabase(); + // Now we have the latest schema, load visibility config map. + loadVisibilityConfigMap(); break; default: // We must did something wrong. @@ -146,30 +127,64 @@ } /** - * Sets visibility settings for the given {@link VisibilityDocument}s. Any previous - * {@link VisibilityDocument}s with same prefixed schema type will be overwritten. + * Sets visibility settings for the given {@link InternalVisibilityConfig}s. Any previous + * {@link InternalVisibilityConfig}s with same prefixed schema type will be overwritten. * - * @param prefixedVisibilityDocuments List of prefixed {@link VisibilityDocument} which - * contains schema type's visibility information. + * @param prefixedVisibilityConfigs List of prefixed {@link InternalVisibilityConfig}s which + * contains schema type's visibility information. * @throws AppSearchException on AppSearchImpl error. */ - public void setVisibility(@NonNull List prefixedVisibilityDocuments) + public void setVisibility(@NonNull List prefixedVisibilityConfigs) throws AppSearchException { - Preconditions.checkNotNull(prefixedVisibilityDocuments); + Preconditions.checkNotNull(prefixedVisibilityConfigs); // Save new setting. - for (int i = 0; i < prefixedVisibilityDocuments.size(); i++) { - // put VisibilityDocument to AppSearchImpl and mVisibilityDocumentMap. If there is a - // VisibilityDocument with same prefixed schema exists, it will be replaced by new - // VisibilityDocument in both AppSearch and memory look up map. - VisibilityDocument prefixedVisibilityDocument = prefixedVisibilityDocuments.get(i); + for (int i = 0; i < prefixedVisibilityConfigs.size(); i++) { + // put VisibilityConfig to AppSearchImpl and mVisibilityConfigMap. If there is a + // VisibilityConfig with same prefixed schema exists, it will be replaced by new + // VisibilityConfig in both AppSearch and memory look up map. + InternalVisibilityConfig prefixedVisibilityConfig = prefixedVisibilityConfigs.get(i); + InternalVisibilityConfig oldVisibilityConfig = + mVisibilityConfigMap.get(prefixedVisibilityConfig.getSchemaType()); mAppSearchImpl.putDocument( VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME, - prefixedVisibilityDocument.toGenericDocument(), + VisibilityToDocumentConverter.createVisibilityDocument( + prefixedVisibilityConfig), /*sendChangeNotifications=*/ false, /*logger=*/ null); - mVisibilityDocumentMap.put(prefixedVisibilityDocument.getId(), - prefixedVisibilityDocument); + + // Put the android V visibility overlay document to AppSearchImpl. + GenericDocument androidVOverlay = + VisibilityToDocumentConverter.createAndroidVOverlay(prefixedVisibilityConfig); + if (androidVOverlay != null) { + mAppSearchImpl.putDocument( + VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + androidVOverlay, + /*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. + // For performance improvement, we should only make the remove call if the old + // VisibilityConfig contains the overlay settings. + try { + mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE, + prefixedVisibilityConfig.getSchemaType(), + /*removeStatsBuilder=*/null); + } catch (AppSearchException e) { + // If it already doesn't exist, that is fine + if (e.getResultCode() != RESULT_NOT_FOUND) { + throw e; + } + } + } + + // Put the VisibilityConfig to memory look up map. + mVisibilityConfigMap.put(prefixedVisibilityConfig.getSchemaType(), + prefixedVisibilityConfig); } // Now that the visibility document has been written. Persist the newly written data. mAppSearchImpl.persistToDisk(PersistType.Code.LITE); @@ -182,12 +197,13 @@ public void removeVisibility(@NonNull Set prefixedSchemaTypes) throws AppSearchException { for (String prefixedSchemaType : prefixedSchemaTypes) { - if (mVisibilityDocumentMap.remove(prefixedSchemaType) != null) { + if (mVisibilityConfigMap.remove(prefixedSchemaType) != null) { // The deleted schema is not all-default setting, we need to remove its // VisibilityDocument from Icing. try { mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME, - VisibilityDocument.NAMESPACE, prefixedSchemaType, + VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE, + prefixedSchemaType, /*removeStatsBuilder=*/null); } catch (AppSearchException e) { if (e.getResultCode() == RESULT_NOT_FOUND) { @@ -195,25 +211,45 @@ // to be fine if we cannot find it. Log.e(TAG, "Cannot find visibility document for " + prefixedSchemaType + " to remove."); - return; + } else { + throw e; } - throw e; + } + + try { + mAppSearchImpl.remove(VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE, + prefixedSchemaType, + /*removeStatsBuilder=*/null); + } catch (AppSearchException e) { + if (e.getResultCode() == RESULT_NOT_FOUND) { + // It's possible no overlay was set, so this this is fine. + if (LogUtil.DEBUG) { + Log.d(TAG, "Cannot find Android V overlay document for " + + prefixedSchemaType + " to remove."); + } + } else { + throw e; + } } } } } - /** Gets the {@link VisibilityDocument} for the given prefixed schema type. */ + /** Gets the {@link InternalVisibilityConfig} for the given prefixed schema type. */ @Nullable - public VisibilityDocument getVisibility(@NonNull String prefixedSchemaType) { - return mVisibilityDocumentMap.get(prefixedSchemaType); + public InternalVisibilityConfig getVisibility(@NonNull String prefixedSchemaType) { + return mVisibilityConfigMap.get(prefixedSchemaType); } /** - * Loads all stored latest {@link VisibilityDocument} from Icing, and put them into - * {@link #mVisibilityDocumentMap}. + * Loads all stored latest {@link InternalVisibilityConfig} from Icing, and put them into + * {@link #mVisibilityConfigMap}. */ - private void loadVisibilityDocumentMap() throws AppSearchException { + @RequiresNonNull("mAppSearchImpl") + private void loadVisibilityConfigMap(@UnderInitialization VisibilityStore this) + throws AppSearchException { // Populate visibility settings set List cachedSchemaTypes = mAppSearchImpl.getAllPrefixedSchemaTypes(); for (int i = 0; i < cachedSchemaTypes.size(); i++) { @@ -223,16 +259,16 @@ continue; // Our own package. Skip. } - VisibilityDocument visibilityDocument; + GenericDocument visibilityDocument; + GenericDocument visibilityAndroidVOverlay = null; try { // Note: We use the other clients' prefixed schema type as ids - visibilityDocument = new VisibilityDocument.Builder( - mAppSearchImpl.getDocument( - VISIBILITY_PACKAGE_NAME, - VISIBILITY_DATABASE_NAME, - VisibilityDocument.NAMESPACE, - /*id=*/ prefixedSchemaType, - /*typePropertyPaths=*/ Collections.emptyMap())).build(); + visibilityDocument = mAppSearchImpl.getDocument( + VISIBILITY_PACKAGE_NAME, + VISIBILITY_DATABASE_NAME, + VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE, + /*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 @@ -242,25 +278,48 @@ // Otherwise, this is some other error we should pass up. throw e; } - mVisibilityDocumentMap.put(prefixedSchemaType, visibilityDocument); + + try { + visibilityAndroidVOverlay = mAppSearchImpl.getDocument( + VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_NAMESPACE, + /*id=*/ prefixedSchemaType, + /*typePropertyPaths=*/ Collections.emptyMap()); + } catch (AppSearchException e) { + if (e.getResultCode() != RESULT_NOT_FOUND) { + // This is some other error we should pass up. + throw e; + } + // Otherwise we continue inserting into visibility document map as the overlay + // map can be null + } + + mVisibilityConfigMap.put( + prefixedSchemaType, + VisibilityToDocumentConverter.createInternalVisibilityConfig( + visibilityDocument, visibilityAndroidVOverlay)); } } /** - * Set the latest version of {@link VisibilityDocument} and its schema to AppSearch. + * Set the latest version of {@link InternalVisibilityConfig} and its schema to AppSearch. */ - private void setLatestSchemaAndDocuments(@NonNull List migratedDocuments) + @RequiresNonNull("mAppSearchImpl") + private void setLatestSchemaAndDocuments( + @UnderInitialization VisibilityStore this, + @NonNull List migratedDocuments) throws AppSearchException { // The latest schema type doesn't exist yet. Add it. Set forceOverride true to // delete old schema. InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME, - Arrays.asList(VisibilityDocument.SCHEMA, - VisibilityPermissionDocument.SCHEMA), - /*visibilityDocuments=*/ Collections.emptyList(), + Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA, + VisibilityPermissionConfig.SCHEMA), + /*visibilityConfigs=*/ Collections.emptyList(), /*forceOverride=*/ true, - /*version=*/ VisibilityDocument.SCHEMA_VERSION_LATEST, + /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST, /*setSchemaStatsBuilder=*/ null); if (!internalSetSchemaResponse.isSuccess()) { // Impossible case, we just set forceOverride to be true, we should never @@ -268,15 +327,185 @@ throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, internalSetSchemaResponse.getErrorMessage()); } + InternalSetSchemaResponse internalSetAndroidVOverlaySchemaResponse = + mAppSearchImpl.setSchema( + VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + Collections.singletonList( + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA), + /*visibilityConfigs=*/ Collections.emptyList(), + /*forceOverride=*/ true, + /*version=*/ VisibilityToDocumentConverter + .ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST, + /*setSchemaStatsBuilder=*/ null); + if (!internalSetAndroidVOverlaySchemaResponse.isSuccess()) { + // Impossible case, we just set forceOverride to be true, we should never + // fail in incompatible changes. + throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, + internalSetAndroidVOverlaySchemaResponse.getErrorMessage()); + } for (int i = 0; i < migratedDocuments.size(); i++) { - VisibilityDocument migratedDocument = migratedDocuments.get(i); - mVisibilityDocumentMap.put(migratedDocument.getId(), migratedDocument); + InternalVisibilityConfig migratedConfig = migratedDocuments.get(i); + mVisibilityConfigMap.put(migratedConfig.getSchemaType(), migratedConfig); mAppSearchImpl.putDocument( VISIBILITY_PACKAGE_NAME, VISIBILITY_DATABASE_NAME, - migratedDocument.toGenericDocument(), + VisibilityToDocumentConverter.createVisibilityDocument(migratedConfig), /*sendChangeNotifications=*/ false, /*logger=*/ null); } } + + /** + * Check and migrate visibility schemas in {@link #ANDROID_V_OVERLAY_DATABASE_NAME} to + * {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST}. + */ + @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)); + 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 + // version. There shouldn't have any public device in this state, so we don't + // actually need to migrate any document. + InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( + VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + Collections.singletonList( + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA), + /*visibilityConfigs=*/ Collections.emptyList(), + /*forceOverride=*/ true, // force update to nest version. + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST, + /*setSchemaStatsBuilder=*/ null); + if (!internalSetSchemaResponse.isSuccess()) { + // Impossible case, we just set forceOverride to be true, we should never + // fail in incompatible changes. + throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, + internalSetSchemaResponse.getErrorMessage()); + } + break; + case VisibilityToDocumentConverter.OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO: + verifyOrSetLatestVisibilityOverlaySchema(getSchemaResponse); + break; + default: + // We must did something wrong. + throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, + "Found unsupported visibility version: " + getSchemaResponse.getVersion()); + } + } + + /** + * Verify the existing visibility schema, set the latest visibilility schema if it's missing. + */ + @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 + // VisibilityOverlay. + + // Check Visibility schema first. + Set existingVisibilitySchema = getSchemaResponse.getSchemas(); + // Force to override visibility schema if it contains DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA. + // The DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA was added to VISIBILITY_DATABASE_NAME and + // removed to ANDROID_V_OVERLAY_DATABASE_NAME. We need to force update the schema to + // migrate devices that have already store public acl schema. + // TODO(b/321326441) remove this method when we no longer to migrate devices in this state. + if (existingVisibilitySchema.contains( + VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA) + && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA) + && existingVisibilitySchema.contains( + VisibilityToDocumentConverter.DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA)) { + InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( + VISIBILITY_PACKAGE_NAME, + VISIBILITY_DATABASE_NAME, + Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA, + VisibilityPermissionConfig.SCHEMA), + /*visibilityConfigs=*/ Collections.emptyList(), + /*forceOverride=*/ true, + /*version=*/ VisibilityToDocumentConverter.SCHEMA_VERSION_LATEST, + /*setSchemaStatsBuilder=*/ null); + if (!internalSetSchemaResponse.isSuccess()) { + throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, + "Fail to force override deprecated visibility schema with public acl."); + } + } else if (!(existingVisibilitySchema.contains( + VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA) + && existingVisibilitySchema.contains(VisibilityPermissionConfig.SCHEMA))) { + // We must have a broken schema. Reset it to the latest version. + // Do NOT set forceOverride to be true here, see comment below. + InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( + VISIBILITY_PACKAGE_NAME, + VISIBILITY_DATABASE_NAME, + Arrays.asList(VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_SCHEMA, + VisibilityPermissionConfig.SCHEMA), + /*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 + // the version number and create a VisibilityStoreMigrationHelper which + // can analyse the different between the old version and the new version + // to migration user's visibility settings. + throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, + "Fail to set the latest visibility schema to AppSearch. " + + "You may need to update the visibility schema version " + + "number."); + } + } + } + + /** + * Verify the existing visibility overlay schema, set the latest overlay schema if it's missing. + */ + @RequiresNonNull("mAppSearchImpl") + private void verifyOrSetLatestVisibilityOverlaySchema( + @UnknownInitialization VisibilityStore this, + @NonNull GetSchemaResponse getAndroidVOverlaySchemaResponse) + throws AppSearchException { + // Check Android V overlay schema. + Set existingAndroidVOverlaySchema = + getAndroidVOverlaySchemaResponse.getSchemas(); + if (!existingAndroidVOverlaySchema.contains( + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA)) { + // We must have a broken schema. Reset it to the latest version. + // Do NOT set forceOverride to be true here, see comment below. + InternalSetSchemaResponse internalSetSchemaResponse = mAppSearchImpl.setSchema( + VISIBILITY_PACKAGE_NAME, + ANDROID_V_OVERLAY_DATABASE_NAME, + Collections.singletonList( + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA), + /*visibilityConfigs=*/ Collections.emptyList(), + /*forceOverride=*/ false, + VisibilityToDocumentConverter.ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST, + /*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 + throw new AppSearchException(AppSearchResult.RESULT_INTERNAL_ERROR, + "Fail to set the overlay visibility schema to AppSearch. " + + "You may need to create new overlay schema."); + } + } + } + + /** + * Whether the given {@link InternalVisibilityConfig} contains Android V overlay settings. + * + *

Android V overlay {@link VisibilityToDocumentConverter#ANDROID_V_OVERLAY_SCHEMA} + * contains public acl and visible to config. + */ + private static boolean isConfigContainsAndroidVOverlay( + @Nullable InternalVisibilityConfig config) { + return config != null + && (config.getVisibilityConfig().getPubliclyVisibleTargetPackage() != null + || !config.getVisibleToConfigs().isEmpty()); + } }

diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
index 4fb4de9..ca255bf 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV0.java
@@ -24,7 +24,6 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -153,7 +152,7 @@
                     deprecatedDocuments.add(appSearchImpl.getDocument(
                             VisibilityStore.VISIBILITY_PACKAGE_NAME,
                             VisibilityStore.VISIBILITY_DATABASE_NAME,
-                            VisibilityDocument.NAMESPACE,
+                            VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                             getDeprecatedVisibilityDocumentId(packageName, databaseName),
                             /*typePropertyPaths=*/ Collections.emptyMap()));
                 } catch (AppSearchException e) {
@@ -206,15 +205,15 @@
                 for (GenericDocument deprecatedPackageDocument : deprecatedPackageDocuments) {
                     String prefixedSchemaType = Preconditions.checkNotNull(
                             deprecatedPackageDocument.getPropertyString(
-                            DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
+                                    DEPRECATED_ACCESSIBLE_SCHEMA_PROPERTY));
                     VisibilityDocumentV1.Builder visibilityBuilder =
                             getOrCreateBuilder(documentBuilderMap, prefixedSchemaType);
                     String packageName = Preconditions.checkNotNull(
                             deprecatedPackageDocument.getPropertyString(
-                                DEPRECATED_PACKAGE_NAME_PROPERTY));
+                                    DEPRECATED_PACKAGE_NAME_PROPERTY));
                     byte[] sha256Cert = Preconditions.checkNotNull(
                             deprecatedPackageDocument.getPropertyBytes(
-                                DEPRECATED_SHA_256_CERT_PROPERTY));
+                                    DEPRECATED_SHA_256_CERT_PROPERTY));
                     visibilityBuilder.addVisibleToPackage(
                             new PackageIdentifier(packageName, sha256Cert));
                 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
index 5a082fc..185c19c 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityStoreMigrationHelperFromV1.java
@@ -20,9 +20,9 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.InternalVisibilityConfig;
 import androidx.appsearch.app.PackageIdentifier;
 import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.VisibilityDocument;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.localstorage.AppSearchImpl;
 import androidx.appsearch.localstorage.util.PrefixUtil;
@@ -67,7 +67,7 @@
                 visibilityDocumentV1s.add(new VisibilityDocumentV1(appSearchImpl.getDocument(
                         VisibilityStore.VISIBILITY_PACKAGE_NAME,
                         VisibilityStore.VISIBILITY_DATABASE_NAME,
-                        VisibilityDocument.NAMESPACE,
+                        VisibilityToDocumentConverter.VISIBILITY_DOCUMENT_NAMESPACE,
                         allPrefixedSchemaTypes.get(i),
                         /*typePropertyPaths=*/ Collections.emptyMap())));
             } catch (AppSearchException e) {
@@ -91,13 +91,13 @@
      * @param visibilityDocumentV1s          The deprecated Visibility Document we found.
      */
     @NonNull
-    static List toVisibilityDocumentsV2(
+    static List toVisibilityDocumentsV2(
             @NonNull List visibilityDocumentV1s) {
-        List latestVisibilityDocuments =
+        List latestVisibilityDocuments =
                 new ArrayList<>(visibilityDocumentV1s.size());
         for (int i = 0; i < visibilityDocumentV1s.size(); i++) {
             VisibilityDocumentV1 visibilityDocumentV1 = visibilityDocumentV1s.get(i);
-            Set> visibleToPermissions = new ArraySet<>();
+            Set> visibleToPermissionSets = new ArraySet<>();
             Set deprecatedVisibleToRoles = visibilityDocumentV1.getVisibleToRoles();
             if (deprecatedVisibleToRoles != null) {
                 for (int deprecatedVisibleToRole : deprecatedVisibleToRoles) {
@@ -111,29 +111,28 @@
                                     .READ_ASSISTANT_APP_SEARCH_DATA);
                             break;
                     }
-                    visibleToPermissions.add(visibleToPermission);
+                    visibleToPermissionSets.add(visibleToPermission);
                 }
             }
             Set deprecatedVisibleToPermissions =
                     visibilityDocumentV1.getVisibleToPermissions();
             if (deprecatedVisibleToPermissions != null) {
-                visibleToPermissions.add(deprecatedVisibleToPermissions);
+                visibleToPermissionSets.add(deprecatedVisibleToPermissions);
             }
 
-            Set packageIdentifiers = new ArraySet<>();
+            InternalVisibilityConfig.Builder latestVisibilityDocumentBuilder =
+                    new InternalVisibilityConfig.Builder(visibilityDocumentV1.getId())
+                            .setNotDisplayedBySystem(visibilityDocumentV1.isNotDisplayedBySystem());
             String[] packageNames = visibilityDocumentV1.getPackageNames();
             byte[][] sha256Certs = visibilityDocumentV1.getSha256Certs();
             if (packageNames.length == sha256Certs.length) {
                 for (int j = 0; j < packageNames.length; j++) {
-                    packageIdentifiers.add(new PackageIdentifier(packageNames[j], sha256Certs[j]));
+                    latestVisibilityDocumentBuilder.addVisibleToPackage(
+                            new PackageIdentifier(packageNames[j], sha256Certs[j]));
                 }
             }
-            VisibilityDocument.Builder latestVisibilityDocumentBuilder =
-                    new VisibilityDocument.Builder(visibilityDocumentV1.getId())
-                    .setNotDisplayedBySystem(visibilityDocumentV1.isNotDisplayedBySystem())
-                    .addVisibleToPackages(packageIdentifiers);
-            if (!visibleToPermissions.isEmpty()) {
-                latestVisibilityDocumentBuilder.setVisibleToPermissions(visibleToPermissions);
+            for (Set visibleToPermissions : visibleToPermissionSets) {
+                latestVisibilityDocumentBuilder.addVisibleToPermissions(visibleToPermissions);
             }
             latestVisibilityDocuments.add(latestVisibilityDocumentBuilder.build());
         }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java
new file mode 100644
index 0000000..c7c5606
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityToDocumentConverter.java
@@ -0,0 +1,450 @@
+/*
+ * 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 androidx.appsearch.localstorage.visibilitystore;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.InternalVisibilityConfig;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.appsearch.app.VisibilityPermissionConfig;
+import androidx.collection.ArraySet;
+
+import com.google.android.appsearch.proto.AndroidVOverlayProto;
+import com.google.android.appsearch.proto.PackageIdentifierProto;
+import com.google.android.appsearch.proto.VisibilityConfigProto;
+import com.google.android.appsearch.proto.VisibleToPermissionProto;
+import com.google.android.icing.protobuf.ByteString;
+import com.google.android.icing.protobuf.InvalidProtocolBufferException;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utilities for working with {@link VisibilityChecker} and {@link VisibilityStore}.
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class VisibilityToDocumentConverter {
+    private static final String TAG = "AppSearchVisibilityToDo";
+
+    /**
+     * 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 = "";
+
+    /**
+     * The Schema type for the Android V visibility setting overlay documents, that allow for
+     * 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";
+
+    /**
+     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
+     */
+    private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
+
+    /** Property that holds the package name that can access a schema. */
+    private static final String VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY = "packageName";
+
+    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
+    private static final String VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY = "sha256Cert";
+
+    /** Property that holds the required permissions to access the schema. */
+    private static final String PERMISSION_PROPERTY = "permission";
+
+    // The initial schema version, one VisibilityConfig contains all visibility information for
+    // whole package.
+    public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
+
+    // One VisibilityConfig contains visibility information for a single schema.
+    public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
+
+    // One VisibilityConfig contains visibility information for a single schema. The permission
+    // visibility information is stored in a document property VisibilityPermissionConfig of the
+    // outer doc.
+    public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
+
+    public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
+
+    // The initial schema version, the overlay schema contains public acl and visible to config
+    // properties.
+    public static final int OVERLAY_SCHEMA_VERSION_PUBLIC_ACL_VISIBLE_TO_CONFIG = 0;
+
+    // The overlay schema only contains a proto property contains all visibility setting.
+    public static final int OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO = 1;
+
+    // The version number of schema saved in Android V overlay database.
+    public static final int ANDROID_V_OVERLAY_SCHEMA_VERSION_LATEST =
+            OVERLAY_SCHEMA_VERSION_ALL_IN_PROTO;
+
+    /**
+     * Schema for the VisibilityStore's documents.
+     *
+     * 

NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}. + */ + public static final AppSearchSchema VISIBILITY_DOCUMENT_SCHEMA = + new AppSearchSchema.Builder(VISIBILITY_DOCUMENT_SCHEMA_TYPE) + .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder( + NOT_DISPLAYED_BY_SYSTEM_PROPERTY) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .build()) + .addProperty(new AppSearchSchema.StringPropertyConfig.Builder( + VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder( + VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder( + PERMISSION_PROPERTY, VisibilityPermissionConfig.SCHEMA_TYPE) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .build(); + + /** Schema for the VisibilityStore's Android V visibility setting overlay. */ + public static final AppSearchSchema ANDROID_V_OVERLAY_SCHEMA = + new AppSearchSchema.Builder(ANDROID_V_OVERLAY_SCHEMA_TYPE) + .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder( + VISIBILITY_PROTO_SERIALIZE_PROPERTY) + .setCardinality(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. + */ + static final AppSearchSchema DEPRECATED_PUBLIC_ACL_OVERLAY_SCHEMA = + new AppSearchSchema.Builder("PublicAclOverlayType") + .addProperty(new AppSearchSchema.StringPropertyConfig.Builder( + "publiclyVisibleTargetPackage") + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .build()) + .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder( + "publiclyVisibleTargetPackageSha256Cert") + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) + .build()) + .build(); + + /** + * Constructs a {@link InternalVisibilityConfig} from two {@link GenericDocument}s. + * + *

This constructor is still needed until we don't treat Visibility related documents as + * {@link GenericDocument}s internally. + * + * @param visibilityDocument a {@link GenericDocument} holding visibility properties + * in {@link #VISIBILITY_DOCUMENT_SCHEMA} + * @param androidVOverlayDocument a {@link GenericDocument} holding visibility properties + * in {@link #ANDROID_V_OVERLAY_SCHEMA} + */ + @NonNull + public static InternalVisibilityConfig createInternalVisibilityConfig( + @NonNull GenericDocument visibilityDocument, + @Nullable GenericDocument androidVOverlayDocument) { + Objects.requireNonNull(visibilityDocument); + + // Parse visibility proto if required + AndroidVOverlayProto androidVOverlayProto = null; + if (androidVOverlayDocument != null) { + try { + byte[] androidVOverlayProtoBytes = androidVOverlayDocument.getPropertyBytes( + VISIBILITY_PROTO_SERIALIZE_PROPERTY); + if (androidVOverlayProtoBytes != null) { + androidVOverlayProto = AndroidVOverlayProto.parseFrom( + androidVOverlayProtoBytes); + } + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Get an invalid android V visibility overlay proto.", e); + } + } + + // Handle all visibility settings other than visibleToConfigs + SchemaVisibilityConfig schemaVisibilityConfig = createVisibilityConfig( + visibilityDocument, androidVOverlayProto); + + // Handle visibleToConfigs + String schemaType = visibilityDocument.getId(); + InternalVisibilityConfig.Builder builder = new InternalVisibilityConfig.Builder(schemaType) + .setNotDisplayedBySystem(visibilityDocument + .getPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY)) + .setVisibilityConfig(schemaVisibilityConfig); + if (androidVOverlayProto != null) { + List visibleToConfigProtoList = + androidVOverlayProto.getVisibleToConfigsList(); + for (int i = 0; i < visibleToConfigProtoList.size(); i++) { + SchemaVisibilityConfig visibleToConfig = + convertVisibilityConfigFromProto(visibleToConfigProtoList.get(i)); + builder.addVisibleToConfig(visibleToConfig); + } + } + + return builder.build(); + } + + /** + * Constructs a {@link SchemaVisibilityConfig} from a {@link GenericDocument} containing legacy + * visibility settings, and an {@link AndroidVOverlayProto} containing extended visibility + * settings. + * + *

This constructor is still needed until we don't treat Visibility related documents as + * {@link GenericDocument}s internally. + * + * @param visibilityDocument a {@link GenericDocument} holding all visibility properties + * other than publiclyVisibleTargetPackage. + * @param androidVOverlayProto the proto containing post-V visibility settings + */ + @NonNull + private static SchemaVisibilityConfig createVisibilityConfig( + @NonNull GenericDocument visibilityDocument, + @Nullable AndroidVOverlayProto androidVOverlayProto) { + Objects.requireNonNull(visibilityDocument); + + // Pre-V visibility settings come from visibilityDocument + SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder(); + + String[] visibleToPackageNames = + visibilityDocument.getPropertyStringArray(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY); + byte[][] visibleToPackageShaCerts = + visibilityDocument.getPropertyBytesArray(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY); + if (visibleToPackageNames != null && visibleToPackageShaCerts != null) { + for (int i = 0; i < visibleToPackageNames.length; i++) { + builder.addAllowedPackage( + new PackageIdentifier( + visibleToPackageNames[i], visibleToPackageShaCerts[i])); + } + } + + GenericDocument[] visibleToPermissionDocs = + visibilityDocument.getPropertyDocumentArray(PERMISSION_PROPERTY); + if (visibleToPermissionDocs != null) { + for (int i = 0; i < visibleToPermissionDocs.length; ++i) { + long[] visibleToPermissionLongs = visibleToPermissionDocs[i].getPropertyLongArray( + VisibilityPermissionConfig.ALL_REQUIRED_PERMISSIONS_PROPERTY); + if (visibleToPermissionLongs != null) { + Set allRequiredPermissions = + new ArraySet<>(visibleToPermissionLongs.length); + for (int j = 0; j < visibleToPermissionLongs.length; j++) { + allRequiredPermissions.add((int) visibleToPermissionLongs[j]); + } + builder.addRequiredPermissions(allRequiredPermissions); + } + } + } + + // Post-V visibility settings come from androidVOverlayProto + if (androidVOverlayProto != null) { + SchemaVisibilityConfig androidVOverlayConfig = + convertVisibilityConfigFromProto( + androidVOverlayProto.getVisibilityConfig()); + builder.setPubliclyVisibleTargetPackage( + androidVOverlayConfig.getPubliclyVisibleTargetPackage()); + } + + return builder.build(); + } + + @NonNull + private static SchemaVisibilityConfig convertVisibilityConfigFromProto( + @NonNull VisibilityConfigProto proto) { + SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder(); + + List visibleToPackageProtoList = proto.getVisibleToPackagesList(); + for (int i = 0; i < visibleToPackageProtoList.size(); i++) { + PackageIdentifierProto visibleToPackage = proto.getVisibleToPackages(i); + builder.addAllowedPackage(convertPackageIdentifierFromProto(visibleToPackage)); + } + + List visibleToPermissionProtoList = + proto.getVisibleToPermissionsList(); + for (int i = 0; i < visibleToPermissionProtoList.size(); i++) { + VisibleToPermissionProto visibleToPermissionProto = visibleToPermissionProtoList.get(i); + Set visibleToPermissions = + new ArraySet<>(visibleToPermissionProto.getPermissionsList()); + builder.addRequiredPermissions(visibleToPermissions); + } + + if (proto.hasPubliclyVisibleTargetPackage()) { + PackageIdentifierProto publiclyVisibleTargetPackage = + proto.getPubliclyVisibleTargetPackage(); + builder.setPubliclyVisibleTargetPackage( + convertPackageIdentifierFromProto(publiclyVisibleTargetPackage)); + } + + return builder.build(); + } + + private static VisibilityConfigProto convertSchemaVisibilityConfigToProto( + @NonNull SchemaVisibilityConfig schemaVisibilityConfig) { + VisibilityConfigProto.Builder builder = VisibilityConfigProto.newBuilder(); + + List visibleToPackages = schemaVisibilityConfig.getAllowedPackages(); + for (int i = 0; i < visibleToPackages.size(); i++) { + PackageIdentifier visibleToPackage = visibleToPackages.get(i); + builder.addVisibleToPackages(convertPackageIdentifierToProto(visibleToPackage)); + } + + Set> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions(); + if (!visibleToPermissions.isEmpty()) { + for (Set allRequiredPermissions : visibleToPermissions) { + builder.addVisibleToPermissions( + VisibleToPermissionProto.newBuilder() + .addAllPermissions(allRequiredPermissions)); + } + } + + PackageIdentifier publicAclPackage = + schemaVisibilityConfig.getPubliclyVisibleTargetPackage(); + if (publicAclPackage != null) { + builder.setPubliclyVisibleTargetPackage( + convertPackageIdentifierToProto(publicAclPackage)); + } + + return builder.build(); + } + + /** + * Returns the {@link GenericDocument} for the visibility schema. + * + * @param config the configuration to populate into the document + */ + @NonNull + public static GenericDocument createVisibilityDocument( + @NonNull InternalVisibilityConfig config) { + GenericDocument.Builder builder = new GenericDocument.Builder<>( + VISIBILITY_DOCUMENT_NAMESPACE, + config.getSchemaType(), // We are using the prefixedSchemaType to be the id + VISIBILITY_DOCUMENT_SCHEMA_TYPE); + builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY, + config.isNotDisplayedBySystem()); + SchemaVisibilityConfig schemaVisibilityConfig = config.getVisibilityConfig(); + List visibleToPackages = schemaVisibilityConfig.getAllowedPackages(); + String[] visibleToPackageNames = new String[visibleToPackages.size()]; + byte[][] visibleToPackageSha256Certs = new byte[visibleToPackages.size()][32]; + for (int i = 0; i < visibleToPackages.size(); i++) { + visibleToPackageNames[i] = visibleToPackages.get(i).getPackageName(); + visibleToPackageSha256Certs[i] = visibleToPackages.get(i).getSha256Certificate(); + } + builder.setPropertyString(VISIBLE_TO_PACKAGE_IDENTIFIER_PROPERTY, visibleToPackageNames); + builder.setPropertyBytes(VISIBLE_TO_PACKAGE_SHA_256_CERT_PROPERTY, + visibleToPackageSha256Certs); + + // Generate an array of GenericDocument for VisibilityPermissionConfig. + Set> visibleToPermissions = schemaVisibilityConfig.getRequiredPermissions(); + if (!visibleToPermissions.isEmpty()) { + GenericDocument[] permissionGenericDocs = + new GenericDocument[visibleToPermissions.size()]; + int i = 0; + for (Set allRequiredPermissions : visibleToPermissions) { + VisibilityPermissionConfig permissionDocument = + new VisibilityPermissionConfig(allRequiredPermissions); + permissionGenericDocs[i++] = permissionDocument.toGenericDocument(); + } + builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs); + } + + // The creationTimestamp doesn't matter for Visibility documents. + // But to make tests pass, we set it 0 so two GenericDocuments generated from + // the same VisibilityConfig can be same. + builder.setCreationTimestampMillis(0L); + + return builder.build(); + } + + /** + * Returns the {@link GenericDocument} for the Android V overlay schema if it is provided, + * null otherwise. + */ + @Nullable + public static GenericDocument createAndroidVOverlay( + @NonNull InternalVisibilityConfig internalVisibilityConfig) { + PackageIdentifier publiclyVisibleTargetPackage = + internalVisibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage(); + Set visibleToConfigs = + internalVisibilityConfig.getVisibleToConfigs(); + if (publiclyVisibleTargetPackage == null && visibleToConfigs.isEmpty()) { + // This config doesn't contains any Android V overlay settings + return null; + } + + VisibilityConfigProto.Builder visibilityConfigProtoBuilder = + VisibilityConfigProto.newBuilder(); + // Set publicAcl + if (publiclyVisibleTargetPackage != null) { + visibilityConfigProtoBuilder.setPubliclyVisibleTargetPackage( + convertPackageIdentifierToProto(publiclyVisibleTargetPackage)); + } + + // Set visibleToConfigs + AndroidVOverlayProto.Builder androidVOverlayProtoBuilder = + AndroidVOverlayProto.newBuilder().setVisibilityConfig(visibilityConfigProtoBuilder); + if (!visibleToConfigs.isEmpty()) { + for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) { + VisibilityConfigProto visibleToConfigProto = + convertSchemaVisibilityConfigToProto(visibleToConfig); + androidVOverlayProtoBuilder.addVisibleToConfigs(visibleToConfigProto); + } + } + + GenericDocument.Builder androidVOverlayBuilder = new GenericDocument.Builder<>( + ANDROID_V_OVERLAY_NAMESPACE, + internalVisibilityConfig.getSchemaType(), + ANDROID_V_OVERLAY_SCHEMA_TYPE) + .setPropertyBytes( + VISIBILITY_PROTO_SERIALIZE_PROPERTY, + androidVOverlayProtoBuilder.build().toByteArray()); + + // The creationTimestamp doesn't matter for Visibility documents. + // But to make tests pass, we set it 0 so two GenericDocuments generated from + // the same VisibilityConfig can be same. + androidVOverlayBuilder.setCreationTimestampMillis(0L); + + return androidVOverlayBuilder.build(); + } + + @NonNull + private static PackageIdentifierProto convertPackageIdentifierToProto( + @NonNull PackageIdentifier packageIdentifier) { + return PackageIdentifierProto.newBuilder() + .setPackageName(packageIdentifier.getPackageName()) + .setPackageSha256Cert(ByteString.copyFrom(packageIdentifier.getSha256Certificate())) + .build(); + } + + @NonNull + private static PackageIdentifier convertPackageIdentifierFromProto( + @NonNull PackageIdentifierProto packageIdentifierProto) { + return new PackageIdentifier( + packageIdentifierProto.getPackageName(), + packageIdentifierProto.getPackageSha256Cert().toByteArray()); + } + + private VisibilityToDocumentConverter() {} +}

diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
index 69d6a9a..075f44e 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/visibilitystore/VisibilityUtil.java
@@ -54,8 +54,11 @@
         Preconditions.checkNotNull(targetPackageName);
         Preconditions.checkNotNull(prefixedSchema);
 
-        if (callerAccess.getCallingPackageName().equals(targetPackageName)) {
-            return true;  // Everyone is always allowed to retrieve their own data.
+        // If the caller is allowed default access to its own data, check if the calling package
+        // and the target package are the same.
+        if (callerAccess.doesCallerHaveSelfAccess()
+                && callerAccess.getCallingPackageName().equals(targetPackageName)) {
+            return true;   // Caller is allowed to retrieve its own data.
         }
         if (visibilityStore == null || visibilityChecker == null) {
             return false;  // No visibility is configured at this time; no other access possible.
diff --git a/appsearch/appsearch-platform-storage/api/current.txt b/appsearch/appsearch-platform-storage/api/current.txt
index 29bfa55..7835f9c 100644
--- a/appsearch/appsearch-platform-storage/api/current.txt
+++ b/appsearch/appsearch-platform-storage/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.appsearch.platformstorage {
 
   @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method @SuppressCompatibility @RequiresApi(35) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static com.google.common.util.concurrent.ListenableFuture createEnterpriseGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
     method public static com.google.common.util.concurrent.ListenableFuture createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
     method public static com.google.common.util.concurrent.ListenableFuture createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
diff --git a/appsearch/appsearch-platform-storage/api/restricted_current.txt b/appsearch/appsearch-platform-storage/api/restricted_current.txt
index 29bfa55..7835f9c 100644
--- a/appsearch/appsearch-platform-storage/api/restricted_current.txt
+++ b/appsearch/appsearch-platform-storage/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.appsearch.platformstorage {
 
   @RequiresApi(android.os.Build.VERSION_CODES.S) public final class PlatformStorage {
+    method @SuppressCompatibility @RequiresApi(35) @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static com.google.common.util.concurrent.ListenableFuture createEnterpriseGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
     method public static com.google.common.util.concurrent.ListenableFuture createGlobalSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.GlobalSearchContext);
     method public static com.google.common.util.concurrent.ListenableFuture createSearchSessionAsync(androidx.appsearch.platformstorage.PlatformStorage.SearchContext);
   }
diff --git a/appsearch/appsearch-platform-storage/build.gradle b/appsearch/appsearch-platform-storage/build.gradle
index 2009937..4db04d8 100644
--- a/appsearch/appsearch-platform-storage/build.gradle
+++ b/appsearch/appsearch-platform-storage/build.gradle
@@ -31,10 +31,10 @@
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
 
-    implementation project(":appsearch:appsearch")
+    implementation(project(":appsearch:appsearch"))
+    implementation(project(":core:core"))
     implementation('androidx.collection:collection:1.2.0')
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
-    implementation("androidx.core:core:1.1.0")
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRules)
@@ -52,5 +52,6 @@
 }
 
 android {
+    compileSdkPreview "VanillaIceCream"
     namespace "androidx.appsearch.platformstorage"
 }
diff --git a/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java
new file mode 100644
index 0000000..0e551e4
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/androidTest/java/androidx/appsearch/platformstorage/util/SchemaValidationUtilTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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 androidx.appsearch.platformstorage.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.collection.ArraySet;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+public class SchemaValidationUtilTest {
+    static final int MAX_SECTIONS_ALLOWED = 64;
+
+    @Test
+    public void testValidate_simpleSchemas() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new BooleanPropertyConfig.Builder("boolProperty")
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new BytesPropertyConfig.Builder("byteProperty")
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {emailSchema, personSchema};
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                MAX_SECTIONS_ALLOWED);
+    }
+
+    @Test
+    public void testValidate_nestedSchemas() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("org", "Organization")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("sender", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("recipient", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("nickname")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema addressSchema = new AppSearchSchema.Builder("Address")
+                .addProperty(new StringPropertyConfig.Builder("streetName")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("zipcode")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).build();
+
+        AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {emailSchema, personSchema, addressSchema, orgSchema};
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                MAX_SECTIONS_ALLOWED);
+    }
+
+    @Test
+    public void testValidate_schemasWithValidCycle() {
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("nickname")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setShouldIndexNestedProperties(false)
+                        .build()
+                ).build();
+
+        AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("employees", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        AppSearchSchema addressSchema = new AppSearchSchema.Builder("Address")
+                .addProperty(new StringPropertyConfig.Builder("streetName")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("zipcode")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {personSchema, orgSchema, addressSchema};
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                MAX_SECTIONS_ALLOWED);
+    }
+
+    @Test
+    public void testValidate_maxSections() {
+        AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
+        for (int i = 0; i < MAX_SECTIONS_ALLOWED; i++) {
+            personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+        Set schemas = new ArraySet<>();
+        schemas.add(personSchemaBuilder.build());
+        // Test that schemas are valid and no exceptions are thrown
+        SchemaValidationUtil.checkSchemasAreValidOrThrow(schemas, MAX_SECTIONS_ALLOWED);
+
+        // Add one more property to bring the number of sections over the max limit
+        personSchemaBuilder.addProperty(new StringPropertyConfig.Builder(
+                "string" + MAX_SECTIONS_ALLOWED + 1)
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                .build());
+        schemas.clear();
+        schemas.add(personSchemaBuilder.build());
+        IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+                () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(schemas,
+                        MAX_SECTIONS_ALLOWED));
+        assertThat(exception.getMessage()).contains("Too many properties to be indexed");
+    }
+
+    @Test
+    public void testValidate_schemasWithInvalidCycleThrowsError() {
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("nickname")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                        .build()
+                ).addProperty(new LongPropertyConfig.Builder("age")
+                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema orgSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("employees", "Person")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {personSchema, orgSchema};
+        IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+                () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                        MAX_SECTIONS_ALLOWED));
+        assertThat(exception.getMessage()).contains("Invalid cycle");
+    }
+
+    @Test
+    public void testValidate_unknownDocumentConfigThrowsError() {
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("subject")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("body")
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("unknown", "Unknown")
+                        .setShouldIndexNestedProperties(true)
+                        .build()
+                ).addProperty(new DocumentPropertyConfig.Builder("unknown2", "Unknown")
+                        .setShouldIndexNestedProperties(false)
+                        .build()
+                ).build();
+
+        AppSearchSchema[] schemas = {emailSchema};
+        IllegalSchemaException exception = assertThrows(IllegalSchemaException.class,
+                () -> SchemaValidationUtil.checkSchemasAreValidOrThrow(new ArraySet<>(schemas),
+                        MAX_SECTIONS_ALLOWED));
+        assertThat(exception.getMessage()).contains("Undefined schema type");
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java
new file mode 100644
index 0000000..b3329a1
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/EnterpriseGlobalSearchSessionImpl.java
@@ -0,0 +1,118 @@
+/*
+ * 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 androidx.appsearch.platformstorage;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.EnterpriseGlobalSearchSession;
+import androidx.appsearch.app.Features;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.app.GetByDocumentIdRequest;
+import androidx.appsearch.app.GetSchemaResponse;
+import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.RequestToPlatformConverter;
+import androidx.appsearch.platformstorage.converter.SearchSpecToPlatformConverter;
+import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
+import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * An implementation of {@link EnterpriseGlobalSearchSession} which proxies to a
+ * platform {@link android.app.appsearch.EnterpriseGlobalSearchSession}.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(35)
+// TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+//  BuildCompat.isAtLeastV() is removed.
[email protected]
+class EnterpriseGlobalSearchSessionImpl implements EnterpriseGlobalSearchSession {
+    private final android.app.appsearch.EnterpriseGlobalSearchSession mPlatformSession;
+    private final Executor mExecutor;
+    private final Features mFeatures;
+
+    EnterpriseGlobalSearchSessionImpl(
+            @NonNull android.app.appsearch.EnterpriseGlobalSearchSession platformSession,
+            @NonNull Executor executor,
+            @NonNull Features features) {
+        mPlatformSession = Preconditions.checkNotNull(platformSession);
+        mExecutor = Preconditions.checkNotNull(executor);
+        mFeatures = Preconditions.checkNotNull(features);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture> getByDocumentIdAsync(
+            @NonNull String packageName, @NonNull String databaseName,
+            @NonNull GetByDocumentIdRequest request) {
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(databaseName);
+        Preconditions.checkNotNull(request);
+        ResolvableFuture> future =
+                ResolvableFuture.create();
+        mPlatformSession.getByDocumentId(packageName, databaseName,
+                RequestToPlatformConverter.toPlatformGetByDocumentIdRequest(request), mExecutor,
+                new BatchResultCallbackAdapter<>(future,
+                        GenericDocumentToPlatformConverter::toJetpackGenericDocument));
+        return future;
+    }
+
+    @Override
+    @NonNull
+    public SearchResults search(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec searchSpec) {
+        Preconditions.checkNotNull(queryExpression);
+        Preconditions.checkNotNull(searchSpec);
+        android.app.appsearch.SearchResults platformSearchResults =
+                mPlatformSession.search(
+                        queryExpression,
+                        SearchSpecToPlatformConverter.toPlatformSearchSpec(searchSpec));
+        return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture getSchemaAsync(@NonNull String packageName,
+            @NonNull String databaseName) {
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(databaseName);
+        ResolvableFuture future = ResolvableFuture.create();
+        mPlatformSession.getSchema(packageName, databaseName, mExecutor,
+                result -> AppSearchResultToPlatformConverter.platformAppSearchResultToFuture(result,
+                        future, GetSchemaResponseToPlatformConverter::toJetpackGetSchemaResponse));
+        return future;
+    }
+
+    @NonNull
+    @Override
+    public Features getFeatures() {
+        return mFeatures;
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 4d7b3f3..b1c4f4e 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -16,15 +16,12 @@
 package androidx.appsearch.platformstorage;
 
 import android.content.Context;
-import android.content.pm.ModuleInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.os.Build;
 
-import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.appsearch.app.Features;
+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 /**
@@ -32,12 +29,6 @@
  * level.
  */
 final class FeaturesImpl implements Features {
-    private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";
-
-    // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to
-    // 0 if it is not found, or the version code if it is found.
-    private static volatile long sAppSearchVersionCode = -1;
-
     // Context is used to check mainline module version, as support varies by module version.
     private final Context mContext;
 
@@ -45,6 +36,9 @@
         mContext = Preconditions.checkNotNull(context);
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     public boolean isFeatureSupported(@NonNull String feature) {
         switch (feature) {
@@ -80,23 +74,39 @@
             case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
                 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
 
-            // Beyond Android U features
+            // Android V Features
             case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
-                // TODO(b/258715421) : Update to reflect support in Android U+ once this feature has
-                // an extservices sdk that includes it.
-                // fall through
-            case Features.SCHEMA_SET_DELETION_PROPAGATION:
-                // TODO(b/268521214) : Update when feature is ready in service-appsearch.
                 // fall through
             case Features.SCHEMA_ADD_PARENT_TYPE:
-                // TODO(b/269295094) : Update when feature is ready in service-appsearch.
                 // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
-                // TODO(b/289150947) : Update when feature is ready in service-appsearch.
                 // fall through
             case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
-                // TODO(b/296088047) : Update when feature is ready in service-appsearch.
-                return false;
+                // fall through
+            case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+                // fall through
+            case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+                // fall through
+            case Features.ENTERPRISE_GLOBAL_SEARCH_SESSION:
+                return BuildCompat.isAtLeastV();
+
+            // Beyond Android V Features
+            case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+                // TODO(b/326656531) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.SCHEMA_SET_DESCRIPTION:
+                // TODO(b/326987971) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+                // TODO(b/332620561) : Update when feature is ready in service-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
+                // TODO(b/332642571) : Update when feature is ready in service-appsearch.
+                // fall through
             default:
                 return false;
         }
@@ -107,53 +117,11 @@
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             return 64;
         } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
-            // Sixty-four properties were enabled in mainline module 'aml_ase_331311020'
-            return getAppSearchVersionCode(mContext) >= 331311020 ? 64 : 16;
+            // Sixty-four properties were enabled in mainline module of the U base version
+            return AppSearchVersionUtil.getAppSearchVersionCode(mContext)
+                    >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE ? 64 : 16;
         } else {
             return 16;
         }
     }
-
-    @RequiresApi(Build.VERSION_CODES.Q)
-    private static long getAppSearchVersionCode(Context context) {
-        if (sAppSearchVersionCode != -1) {
-            return sAppSearchVersionCode;
-        }
-        synchronized (FeaturesImpl.class) {
-            // Check again in case it was assigned while waiting
-            if (sAppSearchVersionCode == -1) {
-                long appsearchVersionCode = 0;
-                try {
-                    PackageManager packageManager = context.getPackageManager();
-                    String appSearchPackageName =
-                            ApiHelperForQ.getAppSearchPackageName(packageManager);
-                    if (appSearchPackageName != null) {
-                        PackageInfo pInfo = packageManager
-                                .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);
-                        appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);
-                    }
-                } catch (PackageManager.NameNotFoundException e) {
-                    // Module not installed
-                }
-                sAppSearchVersionCode = appsearchVersionCode;
-            }
-        }
-        return sAppSearchVersionCode;
-    }
-
-    @RequiresApi(Build.VERSION_CODES.Q)
-    private static class ApiHelperForQ {
-        @DoNotInline
-        static long getPackageInfoLongVersionCode(PackageInfo pInfo) {
-            return pInfo.getLongVersionCode();
-        }
-
-        @DoNotInline
-        static String getAppSearchPackageName(PackageManager packageManager)
-                throws PackageManager.NameNotFoundException {
-            ModuleInfo appSearchModule =
-                    packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);
-            return appSearchModule.getPackageName();
-        }
-    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
index 26c2711c..a5cc45d 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
@@ -47,6 +47,7 @@
 import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
 import androidx.collection.ArrayMap;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -103,6 +104,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public SearchResults search(
@@ -131,6 +135,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     @Override
     public ListenableFuture getSchemaAsync(@NonNull String packageName,
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
index c1ca5d4..0a586c5 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
@@ -20,20 +20,25 @@
 import android.content.Context;
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EnterpriseGlobalSearchSession;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.platformstorage.converter.SearchContextToPlatformConverter;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.function.Consumer;
 
 /**
  * An AppSearch storage system which stores data in the central AppSearch service, available on
@@ -201,7 +206,8 @@
     // execute() won't return anything, we will hang forever waiting for the execution.
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .createCachedThreadPoolExecutor();
 
     /**
      * Opens a new {@link AppSearchSession} on this storage.
@@ -224,7 +230,7 @@
                     if (result.isSuccess()) {
                         future.set(
                                 new SearchSessionImpl(result.getResultValue(), context.mExecutor,
-                                        new FeaturesImpl(context.mContext)));
+                                        context.mContext));
                     } else {
                         // Without the SuppressLint annotation on the method, this line causes a
                         // lint error because getResultCode isn't defined as returning a value from
@@ -266,4 +272,59 @@
                 });
         return future;
     }
+
+    /**
+     * Opens a new {@link EnterpriseGlobalSearchSession} on this storage.
+     */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
+    @RequiresApi(35)
+    @SuppressLint("WrongConstant")
+    @NonNull
+    public static ListenableFuture
+            createEnterpriseGlobalSearchSessionAsync(@NonNull GlobalSearchContext context) {
+        if (!BuildCompat.isAtLeastV()) {
+            throw new UnsupportedOperationException(
+                    Features.ENTERPRISE_GLOBAL_SEARCH_SESSION
+                            + " is not supported on this AppSearch implementation");
+        }
+        Preconditions.checkNotNull(context);
+        AppSearchManager appSearchManager =
+                context.mContext.getSystemService(AppSearchManager.class);
+        ResolvableFuture future = ResolvableFuture.create();
+        ApiHelperForV.createEnterpriseGlobalSearchSession(
+                appSearchManager,
+                context.mExecutor,
+                result -> {
+                    if (result.isSuccess()) {
+                        future.set(new EnterpriseGlobalSearchSessionImpl(
+                                result.getResultValue(), context.mExecutor,
+                                new FeaturesImpl(context.mContext)));
+                    } else {
+                        // Without the SuppressLint annotation on the method, this line causes a
+                        // lint error because getResultCode isn't defined as returning a value from
+                        // AppSearchResult.ResultCode
+                        future.setException(
+                                new AppSearchException(
+                                        result.getResultCode(), result.getErrorMessage()));
+                    }
+                });
+        return future;
+    }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static void createEnterpriseGlobalSearchSession(@NonNull AppSearchManager appSearchManager,
+                @NonNull Executor executor,
+                @NonNull Consumer
+                        android.app.appsearch.EnterpriseGlobalSearchSession>> callback) {
+            appSearchManager.createEnterpriseGlobalSearchSession(executor, callback);
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index d0514b0..d766182 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -15,8 +15,11 @@
  */
 package androidx.appsearch.platformstorage;
 
+import static androidx.appsearch.platformstorage.util.SchemaValidationUtil.checkSchemasAreValidOrThrow;
+
 import android.annotation.SuppressLint;
 import android.app.appsearch.AppSearchResult;
+import android.content.Context;
 import android.os.Build;
 
 import androidx.annotation.DoNotInline;
@@ -40,6 +43,7 @@
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.exceptions.IllegalSchemaException;
 import androidx.appsearch.platformstorage.converter.AppSearchResultToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.GenericDocumentToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.GetSchemaResponseToPlatformConverter;
@@ -49,8 +53,10 @@
 import androidx.appsearch.platformstorage.converter.SearchSuggestionResultToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.SearchSuggestionSpecToPlatformConverter;
 import androidx.appsearch.platformstorage.converter.SetSchemaRequestToPlatformConverter;
+import androidx.appsearch.platformstorage.util.AppSearchVersionUtil;
 import androidx.appsearch.platformstorage.util.BatchResultCallbackAdapter;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -70,22 +76,39 @@
 class SearchSessionImpl implements AppSearchSession {
     private final android.app.appsearch.AppSearchSession mPlatformSession;
     private final Executor mExecutor;
+    private final Context mContext;
     private final Features mFeatures;
 
     SearchSessionImpl(
             @NonNull android.app.appsearch.AppSearchSession platformSession,
             @NonNull Executor executor,
-            @NonNull Features features) {
+            @NonNull Context context) {
         mPlatformSession = Preconditions.checkNotNull(platformSession);
         mExecutor = Preconditions.checkNotNull(executor);
-        mFeatures = Preconditions.checkNotNull(features);
+        mContext = Preconditions.checkNotNull(context);
+        mFeatures = new FeaturesImpl(mContext);
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public ListenableFuture setSchemaAsync(@NonNull SetSchemaRequest request) {
         Preconditions.checkNotNull(request);
         ResolvableFuture future = ResolvableFuture.create();
+        if (needsSchemaValidation()) {
+            try {
+                checkSchemasAreValidOrThrow(request.getSchemas(),
+                        getFeatures().getMaxIndexedProperties());
+            } catch (IllegalSchemaException e) {
+                future.setException(
+                        new AppSearchException(
+                                androidx.appsearch.app.AppSearchResult.RESULT_INVALID_ARGUMENT,
+                                e.getMessage()));
+                return future;
+            }
+        }
         mPlatformSession.setSchema(
                 SetSchemaRequestToPlatformConverter.toPlatformSetSchemaRequest(request),
                 mExecutor,
@@ -97,6 +120,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public ListenableFuture getSchemaAsync() {
@@ -121,6 +147,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public ListenableFuture> putAsync(
@@ -149,6 +178,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @Override
     @NonNull
     public SearchResults search(
@@ -163,6 +195,9 @@
         return new SearchResultsImpl(platformSearchResults, searchSpec, mExecutor);
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     @Override
     public ListenableFuture> searchSuggestionAsync(
@@ -216,6 +251,9 @@
         return future;
     }
 
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @SuppressLint("WrongConstant")
     @Override
     @NonNull
@@ -318,6 +356,17 @@
         mPlatformSession.close();
     }
 
+    private boolean needsSchemaValidation() {
+        long appsearchVersionCode = AppSearchVersionUtil.getAppSearchVersionCode(mContext);
+        // Due to b/300135897, we'd like to validate the schema before sending the setSchema
+        // request to IcingLib on some versions of AppSearch.
+        // For these versions, IcingLib and AppSearch would crash if we try to set an
+        // invalid schema where the number of sections in a schema type exceeds the maximum
+        // limit.
+        return appsearchVersionCode >= AppSearchVersionUtil.APPSEARCH_U_BASE_VERSION_CODE
+                && appsearchVersionCode < AppSearchVersionUtil.APPSEARCH_M2023_11_VERSION_CODE;
+    }
+
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     static class ApiHelperForU {
         private ApiHelperForU() {
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
index 3775ab7..01882d6 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GenericDocumentToPlatformConverter.java
@@ -21,6 +21,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
@@ -87,6 +89,10 @@
                     platformSubDocuments[j] = toPlatformGenericDocument(documentValues[j]);
                 }
                 platformBuilder.setPropertyDocument(propertyName, platformSubDocuments);
+            } else if (property instanceof EmbeddingVector[]) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
             } else {
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s", propertyName,
@@ -143,6 +149,8 @@
                 }
                 jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
             } else {
+                // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes
+                //  available in platform.
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s", propertyName,
                                 property.getClass().toString()));
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
index 2fb4a40..577673e 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/GetSchemaResponseToPlatformConverter.java
@@ -24,9 +24,14 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -43,13 +48,16 @@
      * Translates a platform {@link android.app.appsearch.GetSchemaResponse} into a jetpack
      * {@link GetSchemaResponse}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static GetSchemaResponse toJetpackGetSchemaResponse(
             @NonNull android.app.appsearch.GetSchemaResponse platformResponse) {
         Preconditions.checkNotNull(platformResponse);
         GetSchemaResponse.Builder jetpackBuilder = new GetSchemaResponse.Builder();
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-            // Android API level in S-v2 and lower won't have any supported feature.
+            // Android API level in S-v2 and lower won't have visibility information.
             jetpackBuilder.setVisibilitySettingSupported(false);
         }
         for (android.app.appsearch.AppSearchSchema platformSchema : platformResponse.getSchemas()) {
@@ -72,6 +80,29 @@
                         entry.getValue());
             }
         }
+
+        if (BuildCompat.isAtLeastV()) {
+            // Convert publicly visible schemas
+            Map publiclyVisibleSchemas =
+                    ApiHelperForV.getPubliclyVisibleSchemas(platformResponse);
+            if (!publiclyVisibleSchemas.isEmpty()) {
+                for (Map.Entry entry :
+                        publiclyVisibleSchemas.entrySet()) {
+                    jetpackBuilder.setPubliclyVisibleSchema(entry.getKey(), entry.getValue());
+                }
+            }
+
+            // Convert schemas visible to configs
+            Map> schemasVisibleToConfigs =
+                    ApiHelperForV.getSchemasVisibleToConfigs(platformResponse);
+            if (!schemasVisibleToConfigs.isEmpty()) {
+                for (Map.Entry> entry :
+                        schemasVisibleToConfigs.entrySet()) {
+                    jetpackBuilder.setSchemaTypeVisibleToConfigs(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
         return jetpackBuilder.build();
     }
 
@@ -130,4 +161,95 @@
             return platformResponse.getRequiredPermissionsForSchemaTypeVisibility();
         }
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        @NonNull
+        static Map getPubliclyVisibleSchemas(
+                android.app.appsearch.GetSchemaResponse platformResponse) {
+            Map platformPubliclyVisibleSchemas =
+                    platformResponse.getPubliclyVisibleSchemas();
+            if (platformPubliclyVisibleSchemas.isEmpty()) {
+                return Collections.emptyMap();
+            }
+            Map jetpackPubliclyVisibleSchemas =
+                    new ArrayMap<>(platformPubliclyVisibleSchemas.size());
+            for (Map.Entry entry :
+                    platformPubliclyVisibleSchemas.entrySet()) {
+                jetpackPubliclyVisibleSchemas.put(
+                        entry.getKey(),
+                        new PackageIdentifier(
+                                entry.getValue().getPackageName(),
+                                entry.getValue().getSha256Certificate()));
+            }
+            return jetpackPubliclyVisibleSchemas;
+        }
+
+        @DoNotInline
+        @NonNull
+        static Map> getSchemasVisibleToConfigs(
+                android.app.appsearch.GetSchemaResponse platformResponse) {
+            Map>
+                    platformSchemasVisibleToConfigs =
+                    platformResponse.getSchemaTypesVisibleToConfigs();
+            if (platformSchemasVisibleToConfigs.isEmpty()) {
+                return Collections.emptyMap();
+            }
+            Map> jetpackSchemasVisibleToConfigs =
+                    new ArrayMap<>(platformSchemasVisibleToConfigs.size());
+            for (Map.Entry> entry :
+                    platformSchemasVisibleToConfigs.entrySet()) {
+                Set jetpackConfigPerType =
+                        new ArraySet<>(entry.getValue().size());
+                for (android.app.appsearch.SchemaVisibilityConfig platformConfigPerType :
+                        entry.getValue()) {
+                    SchemaVisibilityConfig jetpackConfig =
+                            toJetpackSchemaVisibilityConfig(platformConfigPerType);
+                    jetpackConfigPerType.add(jetpackConfig);
+                }
+                jetpackSchemasVisibleToConfigs.put(entry.getKey(), jetpackConfigPerType);
+            }
+            return jetpackSchemasVisibleToConfigs;
+        }
+
+        /**
+         * Translates a platform {@link android.app.appsearch.SchemaVisibilityConfig} into a jetpack
+         * {@link SchemaVisibilityConfig}.
+         */
+        @NonNull
+        private static SchemaVisibilityConfig toJetpackSchemaVisibilityConfig(
+                @NonNull android.app.appsearch.SchemaVisibilityConfig platformConfig) {
+            Preconditions.checkNotNull(platformConfig);
+            SchemaVisibilityConfig.Builder jetpackBuilder = new SchemaVisibilityConfig.Builder();
+
+            // Translate allowedPackages
+            List allowedPackages =
+                    platformConfig.getAllowedPackages();
+            for (int i = 0; i < allowedPackages.size(); i++) {
+                jetpackBuilder.addAllowedPackage(new PackageIdentifier(
+                        allowedPackages.get(i).getPackageName(),
+                        allowedPackages.get(i).getSha256Certificate()));
+            }
+
+            // Translate requiredPermissions
+            for (Set requiredPermissions : platformConfig.getRequiredPermissions()) {
+                jetpackBuilder.addRequiredPermissions(requiredPermissions);
+            }
+
+            // Translate publiclyVisibleTargetPackage
+            android.app.appsearch.PackageIdentifier publiclyVisibleTargetPackage =
+                    platformConfig.getPubliclyVisibleTargetPackage();
+            if (publiclyVisibleTargetPackage != null) {
+                jetpackBuilder.setPubliclyVisibleTargetPackage(
+                        new PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+
+            return jetpackBuilder.build();
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
index 83584df..1201088 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/JoinSpecToPlatformConverter.java
@@ -23,6 +23,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.JoinSpec;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 /**
@@ -38,6 +39,9 @@
      */
     @SuppressLint("WrongConstant")
     @NonNull
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     public static android.app.appsearch.JoinSpec toPlatformJoinSpec(@NonNull JoinSpec jetpackSpec) {
         Preconditions.checkNotNull(jetpackSpec);
         return new android.app.appsearch.JoinSpec.Builder(jetpackSpec.getChildPropertyExpression())
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
index f4b2d43..35a4226 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/RequestToPlatformConverter.java
@@ -18,6 +18,7 @@
 
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
@@ -27,6 +28,7 @@
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
 import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.ReportUsageRequest;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -45,16 +47,37 @@
      * Translates a jetpack {@link PutDocumentsRequest} into a platform
      * {@link android.app.appsearch.PutDocumentsRequest}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.PutDocumentsRequest toPlatformPutDocumentsRequest(
             @NonNull PutDocumentsRequest jetpackRequest) {
         Preconditions.checkNotNull(jetpackRequest);
         android.app.appsearch.PutDocumentsRequest.Builder platformBuilder =
                 new android.app.appsearch.PutDocumentsRequest.Builder();
+        // Convert normal generic documents.
         for (GenericDocument jetpackDocument : jetpackRequest.getGenericDocuments()) {
             platformBuilder.addGenericDocuments(
                     GenericDocumentToPlatformConverter.toPlatformGenericDocument(jetpackDocument));
         }
+        // Convert taken action generic documents.
+        for (GenericDocument jetpackTakenActionGenericDocument :
+                jetpackRequest.getTakenActionGenericDocuments()) {
+            if (BuildCompat.isAtLeastV()) {
+                ApiHelperForV.addTakenActionGenericDocuments(
+                        platformBuilder,
+                        GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+                                jetpackTakenActionGenericDocument));
+            } else {
+                // This version of platform-storage doesn't support the dedicated
+                // addTakenActionGenericDocuments API, but we can still add them to the index via
+                // the put API (just without logging).
+                platformBuilder.addGenericDocuments(
+                        GenericDocumentToPlatformConverter.toPlatformGenericDocument(
+                                jetpackTakenActionGenericDocument));
+            }
+        }
         return platformBuilder.build();
     }
 
@@ -71,7 +94,7 @@
                         jetpackRequest.getNamespace())
                         .addIds(jetpackRequest.getIds());
         for (Map.Entry> projection :
-                jetpackRequest.getProjectionsInternal().entrySet()) {
+                jetpackRequest.getProjections().entrySet()) {
             platformBuilder.addProjection(projection.getKey(), projection.getValue());
         }
         return platformBuilder.build();
@@ -122,4 +145,24 @@
                 .setUsageTimestampMillis(jetpackRequest.getUsageTimestampMillis())
                 .build();
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void addTakenActionGenericDocuments(
+                android.app.appsearch.PutDocumentsRequest.Builder platformBuilder,
+                android.app.appsearch.GenericDocument platformTakenActionGenericDocument) {
+            try {
+                platformBuilder.addTakenActionGenericDocuments(platformTakenActionGenericDocument);
+            } catch (android.app.appsearch.exceptions.AppSearchException e) {
+                // This method incorrectly declares that it throws AppSearchException, whereas in
+                // fact there's nothing in its implementation that would do so. Suppress it here
+                // instead of piping all the way through the stack.
+                throw new RuntimeException(
+                        "Unexpected AppSearchException which should not be possible", e);
+            }
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
index 5cf9946..b6ebd2d 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SchemaToPlatformConverter.java
@@ -28,8 +28,10 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.Features;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -46,23 +48,36 @@
      * Translates a jetpack {@link AppSearchSchema} into a platform
      * {@link android.app.appsearch.AppSearchSchema}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.AppSearchSchema toPlatformSchema(
             @NonNull AppSearchSchema jetpackSchema) {
         Preconditions.checkNotNull(jetpackSchema);
         android.app.appsearch.AppSearchSchema.Builder platformBuilder =
                 new android.app.appsearch.AppSearchSchema.Builder(jetpackSchema.getSchemaType());
+        if (!jetpackSchema.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
+        if (!jetpackSchema.getParentTypes().isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE
+                        + " is not available on this AppSearch implementation.");
+            }
+            List parentTypes = jetpackSchema.getParentTypes();
+            for (int i = 0; i < parentTypes.size(); i++) {
+                ApiHelperForV.addParentType(platformBuilder, parentTypes.get(i));
+            }
+        }
         List properties = jetpackSchema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
             android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty =
                     toPlatformProperty(properties.get(i));
             platformBuilder.addProperty(platformProperty);
         }
-        if (!jetpackSchema.getParentTypes().isEmpty()) {
-            // TODO(b/269295094): Remove this once polymorphism becomes available.
-            throw new UnsupportedOperationException(Features.SCHEMA_ADD_PARENT_TYPE
-                    + " is not available on this AppSearch implementation.");
-        }
         return platformBuilder.build();
     }
 
@@ -70,6 +85,9 @@
      * Translates a platform {@link android.app.appsearch.AppSearchSchema} to a jetpack
      * {@link AppSearchSchema}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static AppSearchSchema toJetpackSchema(
             @NonNull android.app.appsearch.AppSearchSchema platformSchema) {
@@ -78,22 +96,36 @@
                 new AppSearchSchema.Builder(platformSchema.getSchemaType());
         List properties =
                 platformSchema.getProperties();
+        // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+        // available in platform.
+        if (BuildCompat.isAtLeastV()) {
+            List parentTypes = ApiHelperForV.getParentTypes(platformSchema);
+            for (int i = 0; i < parentTypes.size(); i++) {
+                jetpackBuilder.addParentType(parentTypes.get(i));
+            }
+        }
         for (int i = 0; i < properties.size(); i++) {
             AppSearchSchema.PropertyConfig jetpackProperty = toJetpackProperty(properties.get(i));
             jetpackBuilder.addProperty(jetpackProperty);
         }
-        // TODO(b/269295094): Call jetpackBuilder.addParentType() to add parent types once
-        //  polymorphism becomes available in platform.
         return jetpackBuilder.build();
     }
 
     // Most stringProperty.get calls cause WrongConstant lint errors because the methods are not
     // defined as returning the same constants as the corresponding setter expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     private static android.app.appsearch.AppSearchSchema.PropertyConfig toPlatformProperty(
             @NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
         Preconditions.checkNotNull(jetpackProperty);
+        if (!jetpackProperty.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
         if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
             AppSearchSchema.StringPropertyConfig stringProperty =
                     (AppSearchSchema.StringPropertyConfig) jetpackProperty;
@@ -110,12 +142,6 @@
                         TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
             }
 
-            if (stringProperty.getDeletionPropagation()) {
-                // TODO(b/268521214): Update once deletion propagation is available.
-                throw new UnsupportedOperationException("Setting deletion propagation is not "
-                        + "supported on this AppSearch implementation.");
-            }
-
             if (stringProperty.getJoinableValueType()
                     == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@@ -163,18 +189,26 @@
         } else if (jetpackProperty instanceof AppSearchSchema.DocumentPropertyConfig) {
             AppSearchSchema.DocumentPropertyConfig documentProperty =
                     (AppSearchSchema.DocumentPropertyConfig) jetpackProperty;
+            android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder platformBuilder =
+                    new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
+                            documentProperty.getName(), documentProperty.getSchemaType())
+                            .setCardinality(documentProperty.getCardinality())
+                            .setShouldIndexNestedProperties(
+                                    documentProperty.shouldIndexNestedProperties());
             if (!documentProperty.getIndexableNestedProperties().isEmpty()) {
-                // TODO(b/289150947): Update and set list once indexable-nested-properties-list is
-                //  available.
-                throw new UnsupportedOperationException(
-                        "DocumentPropertyConfig.addIndexableNestedProperties is not supported on "
-                                + "this AppSearch implementation.");
+                if (!BuildCompat.isAtLeastV()) {
+                    throw new UnsupportedOperationException(
+                            "DocumentPropertyConfig.addIndexableNestedProperties is not supported "
+                                    + "on this AppSearch implementation.");
+                }
+                ApiHelperForV.addIndexableNestedProperties(
+                        platformBuilder, documentProperty.getIndexableNestedProperties());
             }
-            return new android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder(
-                    documentProperty.getName(), documentProperty.getSchemaType())
-                    .setCardinality(documentProperty.getCardinality())
-                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
-                    .build();
+            return platformBuilder.build();
+        } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+            // TODO(b/326656531): Remove this once embedding search APIs are available.
+            throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                    + " is not available on this AppSearch implementation.");
         } else {
             throw new IllegalArgumentException(
                     "Invalid dataType: " + jetpackProperty.getDataType());
@@ -184,6 +218,9 @@
     // Most stringProperty.get calls cause WrongConstant lint errors because the methods are not
     // defined as returning the same constants as the corresponding setter expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     private static AppSearchSchema.PropertyConfig toJetpackProperty(
             @NonNull android.app.appsearch.AppSearchSchema.PropertyConfig platformProperty) {
@@ -201,6 +238,8 @@
                 jetpackBuilder.setJoinableValueType(
                         ApiHelperForU.getJoinableValueType(stringProperty));
             }
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return jetpackBuilder.build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.LongPropertyConfig) {
@@ -213,19 +252,27 @@
                 jetpackBuilder.setIndexingType(
                         ApiHelperForU.getIndexingType(longProperty));
             }
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return jetpackBuilder.build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.DoublePropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.DoublePropertyConfig.Builder(platformProperty.getName())
                     .setCardinality(platformProperty.getCardinality())
                     .build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.BooleanPropertyConfig.Builder(platformProperty.getName())
                     .setCardinality(platformProperty.getCardinality())
                     .build();
         } else if (platformProperty
                 instanceof android.app.appsearch.AppSearchSchema.BytesPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.BytesPropertyConfig.Builder(platformProperty.getName())
                     .setCardinality(platformProperty.getCardinality())
                     .build();
@@ -233,15 +280,24 @@
                 instanceof android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) {
             android.app.appsearch.AppSearchSchema.DocumentPropertyConfig documentProperty =
                     (android.app.appsearch.AppSearchSchema.DocumentPropertyConfig) platformProperty;
-            return new AppSearchSchema.DocumentPropertyConfig.Builder(
-                    documentProperty.getName(),
-                    documentProperty.getSchemaType())
-                    .setCardinality(documentProperty.getCardinality())
-                    .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
-                    .build();
-            // TODO(b/289150947): Add the indexable_nested_properties_list once it becomes
-            //  available in platform.
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
+            AppSearchSchema.DocumentPropertyConfig.Builder jetpackBuilder =
+                    new AppSearchSchema.DocumentPropertyConfig.Builder(
+                            documentProperty.getName(),
+                            documentProperty.getSchemaType())
+                            .setCardinality(documentProperty.getCardinality())
+                            .setShouldIndexNestedProperties(
+                                    documentProperty.shouldIndexNestedProperties());
+            if (BuildCompat.isAtLeastV()) {
+                List indexableNestedProperties =
+                        ApiHelperForV.getIndexableNestedProperties(documentProperty);
+                jetpackBuilder.addIndexableNestedProperties(indexableNestedProperties);
+            }
+            return jetpackBuilder.build();
         } else {
+            // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes
+            //  available in platform.
             throw new IllegalArgumentException(
                     "Invalid property type " + platformProperty.getClass()
                             + ": " + platformProperty);
@@ -288,4 +344,38 @@
             return longPropertyConfig.getIndexingType();
         }
     }
+
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        @SuppressLint("NewApi")
+        static void addParentType(
+                android.app.appsearch.AppSearchSchema.Builder platformBuilder,
+                @NonNull String parentSchemaType) {
+            platformBuilder.addParentType(parentSchemaType);
+        }
+
+        @DoNotInline
+        static void addIndexableNestedProperties(
+                android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder
+                        platformBuilder,
+                @NonNull Collection indexableNestedProperties) {
+            platformBuilder.addIndexableNestedProperties(indexableNestedProperties);
+        }
+
+        @DoNotInline
+        static List getParentTypes(android.app.appsearch.AppSearchSchema platformSchema) {
+            return platformSchema.getParentTypes();
+        }
+
+        @DoNotInline
+        static List getIndexableNestedProperties(
+                android.app.appsearch.AppSearchSchema.DocumentPropertyConfig
+                        platformDocumentProperty) {
+            return platformDocumentProperty.getIndexableNestedProperties();
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
index 47bff5a..38c867c 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -26,6 +26,7 @@
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.SearchSpec;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -46,6 +47,9 @@
     // Most jetpackSearchSpec.get calls cause WrongConstant lint errors because the methods are not
     // defined as returning the same constants as the corresponding setter expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
             @NonNull SearchSpec jetpackSearchSpec) {
@@ -77,15 +81,12 @@
                 .setSnippetCountPerProperty(jetpackSearchSpec.getSnippetCountPerProperty())
                 .setMaxSnippetSize(jetpackSearchSpec.getMaxSnippetSize());
         if (jetpackSearchSpec.getResultGroupingTypeFlags() != 0) {
-            // TODO(b/258715421): Add Build.VERSION.SDK_INT condition once there is an extservices
-            // sdk that includes SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA.
-            if (true) {
-                if ((jetpackSearchSpec.getResultGroupingTypeFlags()
-                        & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0) {
-                    throw new UnsupportedOperationException(
+            if ((jetpackSearchSpec.getResultGroupingTypeFlags()
+                    & SearchSpec.GROUPING_TYPE_PER_SCHEMA) != 0
+                    && !BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
                         Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                            + " is not available on this AppSearch implementation.");
-                }
+                                + " is not available on this AppSearch implementation.");
             }
             platformBuilder.setResultGrouping(
                     jetpackSearchSpec.getResultGroupingTypeFlags(),
@@ -107,6 +108,7 @@
         }
 
         if (!jetpackSearchSpec.getEnabledFeatures().isEmpty()) {
+            // Copy U features
             if (jetpackSearchSpec.isNumericSearchEnabled()
                     || jetpackSearchSpec.isVerbatimSearchEnabled()
                     || jetpackSearchSpec.isListFilterQueryLanguageEnabled()) {
@@ -118,6 +120,27 @@
                 }
                 ApiHelperForU.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
             }
+            // Copy V features
+            if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {
+                if (!BuildCompat.isAtLeastV()) {
+                    throw new UnsupportedOperationException(
+                            Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+                                    + " is not available on this AppSearch implementation.");
+                }
+                ApiHelperForV.copyEnabledFeatures(platformBuilder, jetpackSearchSpec);
+            }
+            // Copy beyond-V features
+            if (jetpackSearchSpec.isEmbeddingSearchEnabled()
+                    || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
+            }
+            if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {
+                // TODO(b/332620561): Remove this once 'tokenize' is supported.
+                throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION
+                        + " is not available on this AppSearch implementation.");
+            }
         }
 
         if (jetpackSearchSpec.getJoinSpec() != null) {
@@ -129,9 +152,29 @@
         }
 
         if (!jetpackSearchSpec.getFilterProperties().isEmpty()) {
-            // TODO(b/296088047): Remove this once property filters become available.
-            throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
-                    + " is not available on this AppSearch implementation.");
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                        + " is not available on this AppSearch implementation.");
+            }
+            ApiHelperForV.addFilterProperties(
+                    platformBuilder, jetpackSearchSpec.getFilterProperties());
+        }
+
+        if (jetpackSearchSpec.getSearchSourceLogTag() != null) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
+                        Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
+                                + " is not available on this AppSearch implementation.");
+            }
+            ApiHelperForV.setSearchSourceLogTag(
+                    platformBuilder, jetpackSearchSpec.getSearchSourceLogTag());
+        }
+
+        if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {
+            // TODO(b/332642571): Remove this once informational ranking expressions are available.
+            throw new UnsupportedOperationException(
+                    Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+                            + " are not available on this AppSearch implementation.");
         }
         return platformBuilder.build();
     }
@@ -143,6 +186,9 @@
         }
 
         @DoNotInline
+        // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+        //  BuildCompat.isAtLeastV() is removed.
+        @BuildCompat.PrereleaseSdkCheck
         static void setJoinSpec(@NonNull android.app.appsearch.SearchSpec.Builder builder,
                 JoinSpec jetpackJoinSpec) {
             builder.setJoinSpec(JoinSpecToPlatformConverter.toPlatformJoinSpec(jetpackJoinSpec));
@@ -176,4 +222,34 @@
             }
         }
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void addFilterProperties(
+                @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,
+                Map> properties) {
+            for (Map.Entry> entry : properties.entrySet()) {
+                platformBuilder.addFilterProperties(entry.getKey(), entry.getValue());
+            }
+        }
+
+        @DoNotInline
+        static void copyEnabledFeatures(
+                @NonNull android.app.appsearch.SearchSpec.Builder platformBuilder,
+                @NonNull SearchSpec jetpackSpec) {
+            if (jetpackSpec.isListFilterHasPropertyFunctionEnabled()) {
+                platformBuilder.setListFilterHasPropertyFunctionEnabled(true);
+            }
+        }
+
+        @DoNotInline
+        static void setSearchSourceLogTag(
+                android.app.appsearch.SearchSpec.Builder platformBuilder,
+                String searchSourceLogTag) {
+            platformBuilder.setSearchSourceLogTag(searchSourceLogTag);
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
index 5479749..ed4a2b2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSuggestionSpecToPlatformConverter.java
@@ -19,12 +19,16 @@
 import android.annotation.SuppressLint;
 import android.os.Build;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.SearchSuggestionSpec;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -44,6 +48,9 @@
     // methods are not defined as returning the same constants as the corresponding setter
     // expects, but they do
     @SuppressLint("WrongConstant")
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SearchSuggestionSpec toPlatformSearchSuggestionSpec(
             @NonNull SearchSuggestionSpec jetpackSearchSuggestionSpec) {
@@ -62,6 +69,32 @@
             platformBuilder.addFilterDocumentIds(documentIdFilters.getKey(),
                     documentIdFilters.getValue());
         }
+
+        Map> jetpackFilterProperties =
+                jetpackSearchSuggestionSpec.getFilterProperties();
+        if (!jetpackFilterProperties.isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                        + " is not available on this AppSearch implementation.");
+            }
+            for (Map.Entry> entry : jetpackFilterProperties.entrySet()) {
+                ApiHelperForV.addFilterProperties(
+                        platformBuilder, entry.getKey(), entry.getValue());
+            }
+        }
         return platformBuilder.build();
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void addFilterProperties(
+                android.app.appsearch.SearchSuggestionSpec.Builder platformBuilder,
+                String schema,
+                Collection propertyPaths) {
+            platformBuilder.addFilterProperties(schema, propertyPaths);
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
index d03d22c..85a1361 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SetSchemaRequestToPlatformConverter.java
@@ -26,10 +26,13 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.app.SetSchemaResponse;
+import androidx.core.os.BuildCompat;
 import androidx.core.util.Preconditions;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -47,6 +50,9 @@
      * Translates a jetpack {@link SetSchemaRequest} into a platform
      * {@link android.app.appsearch.SetSchemaRequest}.
      */
+    // TODO(b/331658692): Remove BuildCompat.PrereleaseSdkCheck annotation once usage of
+    //  BuildCompat.isAtLeastV() is removed.
+    @BuildCompat.PrereleaseSdkCheck
     @NonNull
     public static android.app.appsearch.SetSchemaRequest toPlatformSetSchemaRequest(
             @NonNull SetSchemaRequest jetpackRequest) {
@@ -86,6 +92,38 @@
                 }
             }
         }
+
+        if (!jetpackRequest.getPubliclyVisibleSchemas().isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
+                        "Publicly visible schema are not supported on this AppSearch "
+                                + "implementation.");
+            }
+            for (Map.Entry entry :
+                    jetpackRequest.getPubliclyVisibleSchemas().entrySet()) {
+                PackageIdentifier publiclyVisibleTargetPackage = entry.getValue();
+                ApiHelperForV.setPubliclyVisibleSchema(
+                        platformBuilder,
+                        entry.getKey(),
+                        new android.app.appsearch.PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+        }
+
+        if (!jetpackRequest.getSchemasVisibleToConfigs().isEmpty()) {
+            if (!BuildCompat.isAtLeastV()) {
+                throw new UnsupportedOperationException(
+                        "Schema visible to config are not supported on this AppSearch "
+                                + "implementation.");
+            }
+            for (Map.Entry> entry :
+                    jetpackRequest.getSchemasVisibleToConfigs().entrySet()) {
+                ApiHelperForV.addSchemaTypeVisibleToConfig(
+                        platformBuilder, entry.getKey(), entry.getValue());
+            }
+        }
+
         for (Map.Entry entry : jetpackRequest.getMigrators().entrySet()) {
             Migrator jetpackMigrator = entry.getValue();
             android.app.appsearch.Migrator platformMigrator = new android.app.appsearch.Migrator() {
@@ -176,4 +214,66 @@
             platformBuilder.addRequiredPermissionsForSchemaTypeVisibility(schemaType, permissions);
         }
     }
+
+    @RequiresApi(35)
+    private static class ApiHelperForV {
+        private ApiHelperForV() {}
+
+        @DoNotInline
+        static void setPubliclyVisibleSchema(
+                android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+                String schemaType,
+                android.app.appsearch.PackageIdentifier publiclyVisibleTargetPackage) {
+            platformBuilder.setPubliclyVisibleSchema(schemaType, publiclyVisibleTargetPackage);
+        }
+
+        @DoNotInline
+        public static void addSchemaTypeVisibleToConfig(
+                android.app.appsearch.SetSchemaRequest.Builder platformBuilder,
+                String schemaType,
+                Set jetpackConfigs) {
+            for (SchemaVisibilityConfig jetpackConfig : jetpackConfigs) {
+                android.app.appsearch.SchemaVisibilityConfig platformConfig =
+                        toPlatformSchemaVisibilityConfig(jetpackConfig);
+                platformBuilder.addSchemaTypeVisibleToConfig(schemaType, platformConfig);
+            }
+        }
+
+        /**
+         * Translates a jetpack {@link SchemaVisibilityConfig} into a platform
+         * {@link android.app.appsearch.SchemaVisibilityConfig}.
+         */
+        @NonNull
+        private static android.app.appsearch.SchemaVisibilityConfig
+                toPlatformSchemaVisibilityConfig(@NonNull SchemaVisibilityConfig jetpackConfig) {
+            Preconditions.checkNotNull(jetpackConfig);
+            android.app.appsearch.SchemaVisibilityConfig.Builder platformBuilder =
+                    new android.app.appsearch.SchemaVisibilityConfig.Builder();
+
+            // Translate allowedPackages
+            List allowedPackages = jetpackConfig.getAllowedPackages();
+            for (int i = 0; i < allowedPackages.size(); i++) {
+                platformBuilder.addAllowedPackage(new android.app.appsearch.PackageIdentifier(
+                        allowedPackages.get(i).getPackageName(),
+                        allowedPackages.get(i).getSha256Certificate()));
+            }
+
+            // Translate requiredPermissions
+            for (Set requiredPermissions : jetpackConfig.getRequiredPermissions()) {
+                platformBuilder.addRequiredPermissions(requiredPermissions);
+            }
+
+            // Translate publiclyVisibleTargetPackage
+            PackageIdentifier publiclyVisibleTargetPackage =
+                    jetpackConfig.getPubliclyVisibleTargetPackage();
+            if (publiclyVisibleTargetPackage != null) {
+                platformBuilder.setPubliclyVisibleTargetPackage(
+                        new android.app.appsearch.PackageIdentifier(
+                                publiclyVisibleTargetPackage.getPackageName(),
+                                publiclyVisibleTargetPackage.getSha256Certificate()));
+            }
+
+            return platformBuilder.build();
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java
new file mode 100644
index 0000000..3d778ec
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/AppSearchVersionUtil.java
@@ -0,0 +1,96 @@
+/*
+ * 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 androidx.appsearch.platformstorage.util;
+
+import android.content.Context;
+import android.content.pm.ModuleInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Utilities for retrieving platform AppSearch's module version code.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchVersionUtil {
+    public static final long APPSEARCH_U_BASE_VERSION_CODE = 331311020;
+    public static final long APPSEARCH_M2023_11_VERSION_CODE = 341113000;
+
+    private static final String APPSEARCH_MODULE_NAME = "com.android.appsearch";
+
+    // This will be set to -1 to indicate the AppSearch version code hasn't bee checked, then to
+    // 0 if it is not found, or the version code if it is found.
+    private static volatile long sAppSearchVersionCode = -1;
+
+    private AppSearchVersionUtil() {
+    }
+
+    /**
+     * Returns AppSearch's version code from the context.
+     */
+    @RequiresApi(Build.VERSION_CODES.Q)
+    public static long getAppSearchVersionCode(@NonNull Context context) {
+        Preconditions.checkNotNull(context);
+        if (sAppSearchVersionCode != -1) {
+            return sAppSearchVersionCode;
+        }
+        synchronized (AppSearchVersionUtil.class) {
+            // Check again in case it was assigned while waiting
+            if (sAppSearchVersionCode == -1) {
+                long appsearchVersionCode = 0;
+                try {
+                    PackageManager packageManager = context.getPackageManager();
+                    String appSearchPackageName =
+                            ApiHelperForQ.getAppSearchPackageName(packageManager);
+                    if (appSearchPackageName != null) {
+                        PackageInfo pInfo = packageManager
+                                .getPackageInfo(appSearchPackageName, PackageManager.MATCH_APEX);
+                        appsearchVersionCode = ApiHelperForQ.getPackageInfoLongVersionCode(pInfo);
+                    }
+                } catch (PackageManager.NameNotFoundException e) {
+                    // Module not installed
+                }
+                sAppSearchVersionCode = appsearchVersionCode;
+            }
+        }
+        return sAppSearchVersionCode;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private static class ApiHelperForQ {
+        @DoNotInline
+        static long getPackageInfoLongVersionCode(PackageInfo pInfo) {
+            return pInfo.getLongVersionCode();
+        }
+
+        @DoNotInline
+        static String getAppSearchPackageName(PackageManager packageManager)
+                throws PackageManager.NameNotFoundException {
+            ModuleInfo appSearchModule =
+                    packageManager.getModuleInfo(APPSEARCH_MODULE_NAME, 1);
+            return appSearchModule.getPackageName();
+        }
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java
new file mode 100644
index 0000000..7a98515b
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/util/SchemaValidationUtil.java
@@ -0,0 +1,157 @@
+/*
+ * 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 androidx.appsearch.platformstorage.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utilities for schema validation.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SchemaValidationUtil {
+    private SchemaValidationUtil() {
+    }
+
+    /**
+     * Checks that the set of AppSearch schemas form valid schema-type definitions, and do not
+     * exceed the maximum number of indexed properties allowed.
+     *
+     * @param appSearchSchemas   Set of AppSearch Schemas for a
+     *                           {@link androidx.appsearch.app.SetSchemaRequest}.
+     * @param maxSectionsAllowed The maximum number of sections allowed per AppSearch schema.
+     * @throws IllegalSchemaException if any schema in the set is invalid. A schema is invalid if
+     *                                it contains an invalid cycle, has an undefined document
+     *                                property config, or exceeds the
+     *                                maximum number of sections allowed.
+     */
+    public static void checkSchemasAreValidOrThrow(@NonNull Set appSearchSchemas,
+            int maxSectionsAllowed) throws IllegalSchemaException {
+        Map knownSchemas = new ArrayMap<>();
+        for (AppSearchSchema schema : appSearchSchemas) {
+            knownSchemas.put(schema.getSchemaType(), schema);
+        }
+        Map cachedNumSectionsMap = new ArrayMap<>();
+        for (AppSearchSchema schema : appSearchSchemas) {
+            // Check that the number of sections is below the max allowed
+            int numSections = getNumSectionsInSchemaOrThrow(schema, knownSchemas,
+                    cachedNumSectionsMap, new ArraySet<>());
+            if (numSections > maxSectionsAllowed) {
+                throw new IllegalSchemaException(
+                        "Too many properties to be indexed, max " + "number of properties allowed: "
+                                + maxSectionsAllowed);
+            }
+        }
+    }
+
+    /**
+     * Returns the number of indexes sections in a given AppSearch schema.
+     *
+     * @param schema                    The AppSearch schema to get the number of sections for.
+     * @param knownSchemas              Map of known schema-type strings to their corresponding
+     *                                  schemas.
+     * @param cachedNumSectionsInSchema Map of the cached number of sections in schemas which have
+     *                                  already been expanded.
+     * @param visitedSchemaTypes        Set of schemas that have already been expanded as parents
+     *                                  of the current schema.
+     * @throws IllegalSchemaException if the schema contains an invalid cycle, or contains a
+     *                                DocumentPropertyConfig where the config's schema type is
+     *                                unknown.
+     */
+    private static int getNumSectionsInSchemaOrThrow(@NonNull AppSearchSchema schema,
+            @NonNull Map knownSchemas,
+            @NonNull Map cachedNumSectionsInSchema,
+            @NonNull Set visitedSchemaTypes)
+            throws IllegalSchemaException {
+        String schemaType = schema.getSchemaType();
+        if (visitedSchemaTypes.contains(schemaType)) {
+            // We've hit an illegal cycle where all DocumentPropertyConfigs set
+            // shouldIndexNestedProperties = true.
+            throw new IllegalSchemaException(
+                    "Invalid cycle detected in schema type configs. '" + schemaType
+                            + "' references itself.");
+        }
+        if (cachedNumSectionsInSchema.containsKey(schemaType)) {
+            // We've already calculated and cached the number of sections in this AppSearch schema,
+            // just return this value.
+            return cachedNumSectionsInSchema.get(schemaType);
+        }
+
+        visitedSchemaTypes.add(schemaType);
+        int numSections = 0;
+        for (PropertyConfig property : schema.getProperties()) {
+            if (property.getDataType() == PropertyConfig.DATA_TYPE_DOCUMENT) {
+                DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) property;
+                String docPropertySchemaType = documentProperty.getSchemaType();
+                if (!knownSchemas.containsKey(docPropertySchemaType)) {
+                    // The schema type that this document property config is referring to
+                    // does not exist in the provided schemas
+                    throw new IllegalSchemaException(
+                            "Undefined schema type: " + docPropertySchemaType);
+                }
+                if (!documentProperty.shouldIndexNestedProperties()) {
+                    numSections += documentProperty.getIndexableNestedProperties().size();
+                } else {
+                    numSections += getNumSectionsInSchemaOrThrow(
+                            knownSchemas.get(docPropertySchemaType), knownSchemas,
+                            cachedNumSectionsInSchema, visitedSchemaTypes);
+                }
+            } else {
+                numSections += isPropertyIndexable(property) ? 1 : 0;
+            }
+        }
+        visitedSchemaTypes.remove(schemaType);
+        cachedNumSectionsInSchema.put(schemaType, numSections);
+        return numSections;
+    }
+
+    private static boolean isPropertyIndexable(PropertyConfig propertyConfig) {
+        switch (propertyConfig.getDataType()) {
+            case PropertyConfig.DATA_TYPE_STRING:
+                return ((StringPropertyConfig) propertyConfig).getIndexingType()
+                        != StringPropertyConfig.INDEXING_TYPE_NONE;
+            case PropertyConfig.DATA_TYPE_LONG:
+                return ((LongPropertyConfig) propertyConfig).getIndexingType()
+                        != LongPropertyConfig.INDEXING_TYPE_NONE;
+            case PropertyConfig.DATA_TYPE_DOCUMENT:
+                DocumentPropertyConfig documentProperty = (DocumentPropertyConfig) propertyConfig;
+                return documentProperty.shouldIndexNestedProperties()
+                        || !documentProperty.getIndexableNestedProperties().isEmpty();
+            case PropertyConfig.DATA_TYPE_DOUBLE:
+                // fallthrough
+            case PropertyConfig.DATA_TYPE_BOOLEAN:
+                // fallthrough
+            case PropertyConfig.DATA_TYPE_BYTES:
+                // fallthrough
+            default:
+                return false;
+        }
+    }
+}
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
index 2f580b9..b9a1252 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/FeaturesImpl.java
@@ -45,60 +45,88 @@
             case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
             // Android U Features
             case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
-                // TODO(b/203700301) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/203700301) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.TOKENIZER_TYPE_RFC822:
-                // TODO(b/259294369) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/259294369) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.NUMERIC_SEARCH:
-                // TODO(b/259744228) : Update to reflect support in Android U+ once this feature is
-                // synced over into service-appsearch.
+                // TODO(b/259744228) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
-                // TODO(b/261474063) : Update to reflect support in Android U+ once advanced
-                //  ranking becomes available.
+                // TODO(b/261474063) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.JOIN_SPEC_AND_QUALIFIED_ID:
-                // TODO(b/256022027) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/256022027) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.VERBATIM_SEARCH:
-                // TODO(b/204333391) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/204333391) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.LIST_FILTER_QUERY_LANGUAGE:
-                // TODO(b/208654892) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/208654892) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.LIST_FILTER_HAS_PROPERTY_FUNCTION:
+                // TODO(b/309826655) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG:
+                // TODO(b/326656531) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA:
-                // TODO(b/258715421) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/258715421) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.SEARCH_SUGGESTION:
-                // TODO(b/227356108) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
-                // fall through
-            case Features.SCHEMA_SET_DELETION_PROPAGATION:
-                // TODO(b/268521214) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/227356108) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.SET_SCHEMA_CIRCULAR_REFERENCES:
-                // TODO(b/280698121) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/280698121) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.SCHEMA_ADD_PARENT_TYPE:
-                // TODO(b/269295094) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/269295094) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 // fall through
             case Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES:
-                // TODO(b/289150947) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/289150947) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.SCHEMA_SET_DESCRIPTION:
+                // TODO(b/326987971) : Update to reflect support once this feature is synced over
+                // into gms-appsearch.
                 // fall through
             case Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES:
-                // TODO(b/296088047) : Update to reflect support in Android U+ once this feature is
-                //  synced over into service-appsearch.
+                // TODO(b/296088047) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG:
+                // TODO(b/296088047) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE:
+                // TODO(b/275592563) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG:
+                // TODO(b/275592563) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.LIST_FILTER_TOKENIZE_FUNCTION:
+                // TODO(b/332620561) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
+                // fall through
+            case Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS:
+                // TODO(b/332642571) : Update to reflect support once this feature is synced over
+                //  into gms-appsearch.
                 return false;
             default:
                 return false; // AppSearch features in U+, absent in GMSCore AppSearch.
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
index ca2bee2..8a3556d 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/PlayServicesStorage.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchEnvironmentFactory;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.playservicesstorage.util.AppSearchTaskFutures;
@@ -33,7 +34,6 @@
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 /**
  * An AppSearch storage system which stores data in the central AppSearch service in Google
@@ -200,7 +200,8 @@
     // execute() won't return anything, we will hang forever waiting for the execution.
     // AppSearch multi-thread execution is guarded by Read & Write Lock in AppSearchImpl, all
     // mutate requests will need to gain write lock and query requests need to gain read lock.
-    static final Executor EXECUTOR = Executors.newCachedThreadPool();
+    static final Executor EXECUTOR = AppSearchEnvironmentFactory.getEnvironmentInstance()
+            .createCachedThreadPoolExecutor();
 
     /**
      * Opens a new {@link AppSearchSession} on this storage.
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
index 2b78fbf..0aec84e 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/GenericDocumentToGmsConverter.java
@@ -18,6 +18,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.appsearch.app.EmbeddingVector;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.core.util.Preconditions;
 
@@ -71,6 +73,10 @@
                 }
                 gmsBuilder.setPropertyDocument(propertyName,
                         gmsSubDocuments);
+            } else if (property instanceof EmbeddingVector[]) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
             } else {
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s",
@@ -121,6 +127,8 @@
                 }
                 jetpackBuilder.setPropertyDocument(propertyName, jetpackSubDocuments);
             } else {
+                // TODO(b/326656531) : Add an entry for EmbeddingVector once it becomes
+                //  available in gms-appsearch.
                 throw new IllegalStateException(
                         String.format("Property \"%s\" has unsupported value type %s", propertyName,
                                 property.getClass().toString()));
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
index 34cebb8..2413aad 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/RequestToGmsConverter.java
@@ -68,7 +68,7 @@
                         jetpackRequest.getNamespace())
                         .addIds(jetpackRequest.getIds());
         for (Map.Entry> projection :
-                jetpackRequest.getProjectionsInternal().entrySet()) {
+                jetpackRequest.getProjections().entrySet()) {
             gmsBuilder.addProjection(projection.getKey(), projection.getValue());
         }
         return gmsBuilder.build();
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
index 3c507a5..9c50996 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SchemaToGmsConverter.java
@@ -19,6 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.Features;
 import androidx.core.util.Preconditions;
 
 import java.util.List;
@@ -44,6 +45,11 @@
         com.google.android.gms.appsearch.AppSearchSchema.Builder gmsBuilder =
                 new com.google.android.gms.appsearch.AppSearchSchema
                         .Builder(jetpackSchema.getSchemaType());
+        if (!jetpackSchema.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
         List properties = jetpackSchema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
             com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig gmsProperty =
@@ -64,6 +70,8 @@
         Preconditions.checkNotNull(gmsSchema);
         AppSearchSchema.Builder jetpackBuilder =
                 new AppSearchSchema.Builder(gmsSchema.getSchemaType());
+        // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+        // available in platform.
         List properties =
                 gmsSchema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
@@ -77,6 +85,11 @@
     private static com.google.android.gms.appsearch.AppSearchSchema.PropertyConfig toGmsProperty(
             @NonNull AppSearchSchema.PropertyConfig jetpackProperty) {
         Preconditions.checkNotNull(jetpackProperty);
+        if (!jetpackProperty.getDescription().isEmpty()) {
+            // TODO(b/326987971): Remove this once description becomes available.
+            throw new UnsupportedOperationException(Features.SCHEMA_SET_DESCRIPTION
+                    + " is not available on this AppSearch implementation.");
+        }
         if (jetpackProperty instanceof AppSearchSchema.StringPropertyConfig) {
             AppSearchSchema.StringPropertyConfig stringProperty =
                     (AppSearchSchema.StringPropertyConfig) jetpackProperty;
@@ -87,12 +100,6 @@
                             .setCardinality(stringProperty.getCardinality())
                             .setIndexingType(stringProperty.getIndexingType())
                             .setTokenizerType(stringProperty.getTokenizerType());
-            if (stringProperty.getDeletionPropagation()) {
-                // TODO(b/268521214): Update once deletion propagation is available.
-                throw new UnsupportedOperationException("Setting deletion propagation is not "
-                        + "supported on this AppSearch implementation.");
-            }
-
             if (stringProperty.getJoinableValueType()
                     == AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) {
                 //TODO(b/274986359) Add GMSCore feature check for Joins once available.
@@ -144,6 +151,10 @@
                     .setCardinality(documentProperty.getCardinality())
                     .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
                     .build();
+        } else if (jetpackProperty instanceof AppSearchSchema.EmbeddingPropertyConfig) {
+            // TODO(b/326656531): Remove this once embedding search APIs are available.
+            throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                    + " is not available on this AppSearch implementation.");
         } else {
             throw new IllegalArgumentException(
                     "Invalid dataType: " + jetpackProperty.getDataType());
@@ -160,6 +171,8 @@
             com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig stringProperty =
                     (com.google.android.gms.appsearch.AppSearchSchema.StringPropertyConfig)
                             gmsProperty;
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.StringPropertyConfig.Builder(stringProperty.getName())
                     .setCardinality(stringProperty.getCardinality())
                     .setIndexingType(stringProperty.getIndexingType())
@@ -167,24 +180,32 @@
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.LongPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.LongPropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.DoublePropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.DoublePropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.BooleanPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.BooleanPropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
                     .build();
         } else if (gmsProperty
                 instanceof com.google.android.gms.appsearch.AppSearchSchema.BytesPropertyConfig) {
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.BytesPropertyConfig.Builder(
                     gmsProperty.getName())
                     .setCardinality(gmsProperty.getCardinality())
@@ -196,6 +217,8 @@
                     documentProperty =
                     (com.google.android.gms.appsearch.AppSearchSchema.DocumentPropertyConfig)
                             gmsProperty;
+            // TODO(b/326987971): Call jetpackBuilder.setDescription() once descriptions become
+            // available in platform.
             return new AppSearchSchema.DocumentPropertyConfig.Builder(
                     documentProperty.getName(),
                     documentProperty.getSchemaType())
@@ -203,6 +226,8 @@
                     .setShouldIndexNestedProperties(documentProperty.shouldIndexNestedProperties())
                     .build();
         } else {
+            // TODO(b/326656531) : Add an entry for EmbeddingPropertyConfig once it becomes
+            //  available in gms-appsearch.
             throw new IllegalArgumentException(
                     "Invalid property type " + gmsProperty.getClass()
                             + ": " + gmsProperty);
diff --git a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
index 1b7701b..583885e 100644
--- a/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
+++ b/appsearch/appsearch-play-services-storage/src/main/java/androidx/appsearch/playservicesstorage/converter/SearchSpecToGmsConverter.java
@@ -83,6 +83,22 @@
                                 + "LIST_FILTER_QUERY_LANGUAGE) are not supported with this "
                                 + "backend/Android API level combination.");
             }
+            if (jetpackSearchSpec.isListFilterHasPropertyFunctionEnabled()) {
+                // TODO(b/309826655): Remove this once the hasProperty function becomes available.
+                throw new UnsupportedOperationException(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+                        + " is not available on this AppSearch implementation.");
+            }
+            if (jetpackSearchSpec.isEmbeddingSearchEnabled()
+                    || !jetpackSearchSpec.getSearchEmbeddings().isEmpty()) {
+                // TODO(b/326656531): Remove this once embedding search APIs are available.
+                throw new UnsupportedOperationException(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                        + " is not available on this AppSearch implementation.");
+            }
+            if (jetpackSearchSpec.isListFilterTokenizeFunctionEnabled()) {
+                // TODO(b/332620561): Remove this once 'tokenize' is supported.
+                throw new UnsupportedOperationException(Features.LIST_FILTER_TOKENIZE_FUNCTION
+                        + " is not available on this AppSearch implementation.");
+            }
         }
 
         if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
@@ -104,6 +120,13 @@
                     + " is not available on this AppSearch implementation.");
         }
 
+        if (!jetpackSearchSpec.getInformationalRankingExpressions().isEmpty()) {
+            // TODO(b/332642571): Remove this once informational ranking expressions are available.
+            throw new UnsupportedOperationException(
+                    Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+                            + " are not available on this AppSearch implementation.");
+        }
+
         return gmsBuilder.build();
     }
 }
diff --git a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
index 6d996e9..7391cbb 100644
--- a/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
+++ b/appsearch/appsearch-test-util/src/main/java/androidx/appsearch/testutil/AppSearchTestUtils.java
@@ -27,7 +27,9 @@
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.localstorage.visibilitystore.CallerAccess;
 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -115,14 +117,56 @@
     }
 
     /**
-     * Creates a mock {@link VisibilityChecker}.
+     * Creates a mock {@link VisibilityChecker} where schema is searchable if prefixedSchema is
+     * one of the provided set of visiblePrefixedSchemas and caller does not have system access.
+     *
      * @param visiblePrefixedSchemas Schema types that are accessible to any caller.
-     * @return
+     * @return Mocked {@link VisibilityChecker} instance.
      */
     @NonNull
     public static VisibilityChecker createMockVisibilityChecker(
             @NonNull Set visiblePrefixedSchemas) {
-        return (callerAccess, packageName, prefixedSchema, visibilityStore) ->
-                visiblePrefixedSchemas.contains(prefixedSchema);
+        return new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(
+                    @NonNull CallerAccess callerAccess,
+                    @NonNull String packageName,
+                    @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return visiblePrefixedSchemas.contains(prefixedSchema);
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String s) {
+                return false;
+            }
+        };
+    }
+
+    /**
+     * Creates a mock {@link VisibilityChecker}, where it can be configured if schema is searchable
+     * by caller and caller does not have system access.
+     *
+     * @param isSchemaSearchableByCaller Schema visibility for caller.
+     * @return Mocked {@link VisibilityChecker} instance.
+     */
+    @NonNull
+    public static VisibilityChecker createMockVisibilityChecker(
+            boolean isSchemaSearchableByCaller) {
+        return new VisibilityChecker() {
+            @Override
+            public boolean isSchemaSearchableByCaller(
+                    @NonNull CallerAccess callerAccess,
+                    @NonNull String packageName,
+                    @NonNull String prefixedSchema,
+                    @NonNull VisibilityStore visibilityStore) {
+                return isSchemaSearchableByCaller;
+            }
+
+            @Override
+            public boolean doesCallerHaveSystemAccess(@NonNull String s) {
+                return false;
+            }
+        };
     }
 }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index aff9289..9eff333 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -35,6 +35,12 @@
     method public abstract boolean required() default false;
   }
 
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
   @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
   }
 
@@ -115,11 +121,14 @@
     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 public static final int RESULT_RATE_LIMITED = 10; // 0xa
     field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+    field public static final int RESULT_TIMED_OUT = 11; // 0xb
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
   public final class AppSearchSchema {
+    method public String getDescription();
     method public java.util.List getParentTypes();
     method public java.util.List getProperties();
     method public String getSchemaType();
@@ -132,6 +141,7 @@
     ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.Builder {
@@ -139,6 +149,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -148,6 +159,7 @@
     ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -164,6 +176,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
   }
 
@@ -174,6 +187,21 @@
     ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.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
+  }
+
+  public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+    ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
   }
 
   public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -186,11 +214,13 @@
     ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
   }
 
   public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
+    method public String getDescription();
     method public String getName();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -198,7 +228,6 @@
   }
 
   public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
-    method public boolean getDeletionPropagation();
     method public int getIndexingType();
     method public int getJoinableValueType();
     method public int getTokenizerType();
@@ -217,7 +246,7 @@
     ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -248,25 +277,47 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
+    ctor public EmbeddingVector(float[], String);
+    method public String getModelSignature();
+    method public float[] getValues();
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {
+    method public com.google.common.util.concurrent.ListenableFuture!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
+    method public androidx.appsearch.app.Features getFeatures();
+    method public com.google.common.util.concurrent.ListenableFuture getSchemaAsync(String, String);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
   public interface Features {
     method public int getMaxIndexedProperties();
     method public boolean isFeatureSupported(String);
     field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
     field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
     field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+    field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
     field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+    field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
     field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
-    field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+    field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+    field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+    field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
     field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
     field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
     field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+    field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
     field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
     field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+    field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+    field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
     field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
   }
@@ -287,6 +338,8 @@
     method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String);
     method public double getPropertyDouble(String);
     method public double[]? getPropertyDoubleArray(String);
+    method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);
+    method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String);
     method public long getPropertyLong(String);
     method public long[]? getPropertyLongArray(String);
     method public java.util.Set getPropertyNames();
@@ -301,6 +354,7 @@
   }
 
   public static class GenericDocument.Builder {
+    ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument);
     ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType clearProperty(String);
@@ -311,6 +365,7 @@
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
     method public BuilderType setPropertyDouble(String, double...);
+    method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
     method public BuilderType setSchemaType(String);
@@ -336,8 +391,10 @@
   }
 
   public final class GetSchemaResponse {
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map getPubliclyVisibleSchemas();
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set getSchemaTypesNotDisplayedBySystem();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToConfigs();
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToPackages();
     method public java.util.Set getSchemas();
     method @IntRange(from=0) public int getVersion();
@@ -348,7 +405,9 @@
     method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
     method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
     method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set!>);
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToConfigs(String, java.util.Set);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);
@@ -423,6 +482,7 @@
 
   public final class PutDocumentsRequest {
     method public java.util.List getGenericDocuments();
+    method public java.util.List getTakenActionGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
@@ -431,6 +491,8 @@
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
@@ -472,11 +534,28 @@
     method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
+  public final class SchemaVisibilityConfig {
+    method public java.util.List getAllowedPackages();
+    method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();
+    method public java.util.Set!> getRequiredPermissions();
+  }
+
+  public static final class SchemaVisibilityConfig.Builder {
+    ctor public SchemaVisibilityConfig.Builder();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set);
+    method public androidx.appsearch.app.SchemaVisibilityConfig build();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);
+  }
+
   public final class SearchResult {
     method public String getDatabaseName();
     method public  T getDocument(Class) throws androidx.appsearch.exceptions.AppSearchException;
     method public  T getDocument(Class, java.util.Map!>?) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List getInformationalRankingSignals();
     method public java.util.List getJoinedResults();
     method public java.util.List getMatchInfos();
     method public String getPackageName();
@@ -485,6 +564,7 @@
 
   public static final class SearchResult.Builder {
     ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double);
     method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult);
     method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
     method public androidx.appsearch.app.SearchResult build();
@@ -526,9 +606,12 @@
 
   public final class SearchSpec {
     method public String getAdvancedRankingExpression();
+    method public int getDefaultEmbeddingSearchMetricType();
     method public java.util.List getFilterNamespaces();
     method public java.util.List getFilterPackageNames();
+    method public java.util.Map!> getFilterProperties();
     method public java.util.List getFilterSchemas();
+    method public java.util.List getInformationalRankingExpressions();
     method public androidx.appsearch.app.JoinSpec? getJoinSpec();
     method public int getMaxSnippetSize();
     method public int getOrder();
@@ -540,18 +623,26 @@
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
     method public int getResultGroupingTypeFlags();
+    method public java.util.List getSearchEmbeddings();
+    method public String? getSearchSourceLogTag();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    method public boolean isEmbeddingSearchEnabled();
+    method public boolean isListFilterHasPropertyFunctionEnabled();
     method public boolean isListFilterQueryLanguageEnabled();
+    method public boolean isListFilterTokenizeFunctionEnabled();
     method public boolean isNumericSearchEnabled();
     method public boolean isVerbatimSearchEnabled();
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+    field 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
-    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+    field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
@@ -562,6 +653,7 @@
     field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
     field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
     field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
+    field public static final String SCHEMA_TYPE_WILDCARD = "*";
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -574,15 +666,27 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection);
     method public androidx.appsearch.app.SearchSpec build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
@@ -594,6 +698,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
@@ -613,6 +718,7 @@
   public final class SearchSuggestionSpec {
     method public java.util.Map!> getFilterDocumentIds();
     method public java.util.List getFilterNamespaces();
+    method public java.util.Map!> getFilterProperties();
     method public java.util.List getFilterSchemas();
     method public int getMaximumResultCount();
     method public int getRankingStrategy();
@@ -629,6 +735,10 @@
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection);
     method public androidx.appsearch.app.SearchSuggestionSpec build();
@@ -637,9 +747,11 @@
 
   public final class SetSchemaRequest {
     method public java.util.Map getMigrators();
+    method public java.util.Map getPubliclyVisibleSchemas();
     method public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method public java.util.Set getSchemas();
     method public java.util.Set getSchemasNotDisplayedBySystem();
+    method public java.util.Map!> getSchemasVisibleToConfigs();
     method public java.util.Map!> getSchemasVisibleToPackages();
     method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
@@ -653,26 +765,32 @@
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection!>) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class, java.util.Set) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig);
     method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
     method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection);
     method public androidx.appsearch.app.SetSchemaRequest build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class, boolean) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
   }
 
-  public class SetSchemaResponse {
+  public final class SetSchemaResponse {
     method public java.util.Set getDeletedTypes();
     method public java.util.Set getIncompatibleTypes();
     method public java.util.Set getMigratedTypes();
@@ -700,7 +818,7 @@
     method public String getSchemaType();
   }
 
-  public class StorageInfo {
+  public final class StorageInfo {
     method public int getAliveDocumentsCount();
     method public int getAliveNamespacesCount();
     method public long getSizeBytes();
@@ -771,6 +889,51 @@
 
 }
 
+package androidx.appsearch.usagereporting {
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public String? getQuery();
+    method public String? getReferencedQualifiedId();
+    method public int getResultRankGlobal();
+    method public int getResultRankInBlock();
+    method public long getTimeStayOnResultMillis();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {
+    ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);
+    ctor public ClickAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.ClickAction build();
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public int getFetchedResultCount();
+    method public String? getQuery();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {
+    ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);
+    ctor public SearchAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.SearchAction build();
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {
+    method public long getActionTimestampMillis();
+    method public long getDocumentTtlMillis();
+    method public String getId();
+    method public String getNamespace();
+  }
+
+}
+
 package androidx.appsearch.util {
 
   public class DocumentIdUtil {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index aff9289..9eff333 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -35,6 +35,12 @@
     method public abstract boolean required() default false;
   }
 
+  @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.EmbeddingProperty {
+    method public abstract int indexingType() default androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE;
+    method public abstract String name() default "";
+    method public abstract boolean required() default false;
+  }
+
   @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public static @interface Document.Id {
   }
 
@@ -115,11 +121,14 @@
     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 public static final int RESULT_RATE_LIMITED = 10; // 0xa
     field public static final int RESULT_SECURITY_ERROR = 8; // 0x8
+    field public static final int RESULT_TIMED_OUT = 11; // 0xb
     field public static final int RESULT_UNKNOWN_ERROR = 1; // 0x1
   }
 
   public final class AppSearchSchema {
+    method public String getDescription();
     method public java.util.List getParentTypes();
     method public java.util.List getProperties();
     method public String getSchemaType();
@@ -132,6 +141,7 @@
     ctor public AppSearchSchema.BooleanPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BooleanPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.Builder {
@@ -139,6 +149,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_PARENT_TYPE) public androidx.appsearch.app.AppSearchSchema.Builder addParentType(String);
     method public androidx.appsearch.app.AppSearchSchema.Builder addProperty(androidx.appsearch.app.AppSearchSchema.PropertyConfig);
     method public androidx.appsearch.app.AppSearchSchema build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.BytesPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -148,6 +159,7 @@
     ctor public AppSearchSchema.BytesPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.BytesPropertyConfig.Builder setDescription(String);
   }
 
   public static final class AppSearchSchema.DocumentPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -164,6 +176,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths(java.util.Collection);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.DocumentPropertyConfig.Builder setShouldIndexNestedProperties(boolean);
   }
 
@@ -174,6 +187,21 @@
     ctor public AppSearchSchema.DoublePropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.DoublePropertyConfig.Builder setDescription(String);
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public static final class AppSearchSchema.EmbeddingPropertyConfig extends androidx.appsearch.app.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
+  }
+
+  public static final class AppSearchSchema.EmbeddingPropertyConfig.Builder {
+    ctor public AppSearchSchema.EmbeddingPropertyConfig.Builder(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig build();
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setDescription(String);
+    method public androidx.appsearch.app.AppSearchSchema.EmbeddingPropertyConfig.Builder setIndexingType(int);
   }
 
   public static final class AppSearchSchema.LongPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
@@ -186,11 +214,13 @@
     ctor public AppSearchSchema.LongPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setCardinality(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.LongPropertyConfig.Builder setIndexingType(int);
   }
 
   public abstract static class AppSearchSchema.PropertyConfig {
     method public int getCardinality();
+    method public String getDescription();
     method public String getName();
     field public static final int CARDINALITY_OPTIONAL = 2; // 0x2
     field public static final int CARDINALITY_REPEATED = 1; // 0x1
@@ -198,7 +228,6 @@
   }
 
   public static final class AppSearchSchema.StringPropertyConfig extends androidx.appsearch.app.AppSearchSchema.PropertyConfig {
-    method public boolean getDeletionPropagation();
     method public int getIndexingType();
     method public int getJoinableValueType();
     method public int getTokenizerType();
@@ -217,7 +246,7 @@
     ctor public AppSearchSchema.StringPropertyConfig.Builder(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig build();
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setCardinality(int);
-    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DELETION_PROPAGATION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDeletionPropagation(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_SET_DESCRIPTION) public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setDescription(String);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setIndexingType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setJoinableValueType(int);
     method public androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.Builder setTokenizerType(int);
@@ -248,25 +277,47 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public final class EmbeddingVector {
+    ctor public EmbeddingVector(float[], String);
+    method public String getModelSignature();
+    method public float[] getValues();
+  }
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) public interface EnterpriseGlobalSearchSession {
+    method public com.google.common.util.concurrent.ListenableFuture!> getByDocumentIdAsync(String, String, androidx.appsearch.app.GetByDocumentIdRequest);
+    method public androidx.appsearch.app.Features getFeatures();
+    method public com.google.common.util.concurrent.ListenableFuture getSchemaAsync(String, String);
+    method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
+  }
+
   public interface Features {
     method public int getMaxIndexedProperties();
     method public boolean isFeatureSupported(String);
     field public static final String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
+    field public static final String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION";
     field public static final String GLOBAL_SEARCH_SESSION_GET_BY_ID = "GLOBAL_SEARCH_SESSION_GET_BY_ID";
     field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
+    field public static final String LIST_FILTER_HAS_PROPERTY_FUNCTION = "LIST_FILTER_HAS_PROPERTY_FUNCTION";
     field public static final String LIST_FILTER_QUERY_LANGUAGE = "LIST_FILTER_QUERY_LANGUAGE";
+    field public static final String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES";
     field public static final String SCHEMA_ADD_PARENT_TYPE = "SCHEMA_ADD_PARENT_TYPE";
-    field public static final String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION";
+    field public static final String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG";
+    field public static final String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES";
+    field public static final String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS";
     field public static final String SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION = "SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION";
     field public static final String SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA = "SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA";
     field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+    field public static final String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG";
     field public static final String SEARCH_SUGGESTION = "SEARCH_SUGGESTION";
     field public static final String SET_SCHEMA_CIRCULAR_REFERENCES = "SET_SCHEMA_CIRCULAR_REFERENCES";
+    field public static final String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG";
+    field public static final String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
     field public static final String VERBATIM_SEARCH = "VERBATIM_SEARCH";
   }
@@ -287,6 +338,8 @@
     method public androidx.appsearch.app.GenericDocument![]? getPropertyDocumentArray(String);
     method public double getPropertyDouble(String);
     method public double[]? getPropertyDoubleArray(String);
+    method public androidx.appsearch.app.EmbeddingVector? getPropertyEmbedding(String);
+    method public androidx.appsearch.app.EmbeddingVector![]? getPropertyEmbeddingArray(String);
     method public long getPropertyLong(String);
     method public long[]? getPropertyLongArray(String);
     method public java.util.Set getPropertyNames();
@@ -301,6 +354,7 @@
   }
 
   public static class GenericDocument.Builder {
+    ctor public GenericDocument.Builder(androidx.appsearch.app.GenericDocument);
     ctor public GenericDocument.Builder(String, String, String);
     method public androidx.appsearch.app.GenericDocument build();
     method public BuilderType clearProperty(String);
@@ -311,6 +365,7 @@
     method public BuilderType setPropertyBytes(String, byte[]!...);
     method public BuilderType setPropertyDocument(String, androidx.appsearch.app.GenericDocument!...);
     method public BuilderType setPropertyDouble(String, double...);
+    method public BuilderType setPropertyEmbedding(String, androidx.appsearch.app.EmbeddingVector!...);
     method public BuilderType setPropertyLong(String, long...);
     method public BuilderType setPropertyString(String, java.lang.String!...);
     method public BuilderType setSchemaType(String);
@@ -336,8 +391,10 @@
   }
 
   public final class GetSchemaResponse {
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map getPubliclyVisibleSchemas();
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Set getSchemaTypesNotDisplayedBySystem();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToConfigs();
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public java.util.Map!> getSchemaTypesVisibleToPackages();
     method public java.util.Set getSchemas();
     method @IntRange(from=0) public int getVersion();
@@ -348,7 +405,9 @@
     method public androidx.appsearch.app.GetSchemaResponse.Builder addSchema(androidx.appsearch.app.AppSearchSchema);
     method public androidx.appsearch.app.GetSchemaResponse.Builder addSchemaTypeNotDisplayedBySystem(String);
     method public androidx.appsearch.app.GetSchemaResponse build();
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set!>);
+    method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToConfigs(String, java.util.Set);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setSchemaTypeVisibleToPackages(String, java.util.Set);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVersion(@IntRange(from=0) int);
     method public androidx.appsearch.app.GetSchemaResponse.Builder setVisibilitySettingSupported(boolean);
@@ -423,6 +482,7 @@
 
   public final class PutDocumentsRequest {
     method public java.util.List getGenericDocuments();
+    method public java.util.List getTakenActionGenericDocuments();
   }
 
   public static final class PutDocumentsRequest.Builder {
@@ -431,6 +491,8 @@
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addDocuments(java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(androidx.appsearch.app.GenericDocument!...);
     method public androidx.appsearch.app.PutDocumentsRequest.Builder addGenericDocuments(java.util.Collection);
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(androidx.appsearch.usagereporting.TakenAction!...) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.PutDocumentsRequest.Builder addTakenActions(java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.PutDocumentsRequest build();
   }
 
@@ -472,11 +534,28 @@
     method public androidx.appsearch.app.ReportUsageRequest.Builder setUsageTimestampMillis(long);
   }
 
+  public final class SchemaVisibilityConfig {
+    method public java.util.List getAllowedPackages();
+    method public androidx.appsearch.app.PackageIdentifier? getPubliclyVisibleTargetPackage();
+    method public java.util.Set!> getRequiredPermissions();
+  }
+
+  public static final class SchemaVisibilityConfig.Builder {
+    ctor public SchemaVisibilityConfig.Builder();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addAllowedPackage(androidx.appsearch.app.PackageIdentifier);
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder addRequiredPermissions(java.util.Set);
+    method public androidx.appsearch.app.SchemaVisibilityConfig build();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearAllowedPackages();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder clearRequiredPermissions();
+    method public androidx.appsearch.app.SchemaVisibilityConfig.Builder setPubliclyVisibleTargetPackage(androidx.appsearch.app.PackageIdentifier?);
+  }
+
   public final class SearchResult {
     method public String getDatabaseName();
     method public  T getDocument(Class) throws androidx.appsearch.exceptions.AppSearchException;
     method public  T getDocument(Class, java.util.Map!>?) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.GenericDocument getGenericDocument();
+    method public java.util.List getInformationalRankingSignals();
     method public java.util.List getJoinedResults();
     method public java.util.List getMatchInfos();
     method public String getPackageName();
@@ -485,6 +564,7 @@
 
   public static final class SearchResult.Builder {
     ctor public SearchResult.Builder(String, String);
+    method public androidx.appsearch.app.SearchResult.Builder addInformationalRankingSignal(double);
     method public androidx.appsearch.app.SearchResult.Builder addJoinedResult(androidx.appsearch.app.SearchResult);
     method public androidx.appsearch.app.SearchResult.Builder addMatchInfo(androidx.appsearch.app.SearchResult.MatchInfo);
     method public androidx.appsearch.app.SearchResult build();
@@ -526,9 +606,12 @@
 
   public final class SearchSpec {
     method public String getAdvancedRankingExpression();
+    method public int getDefaultEmbeddingSearchMetricType();
     method public java.util.List getFilterNamespaces();
     method public java.util.List getFilterPackageNames();
+    method public java.util.Map!> getFilterProperties();
     method public java.util.List getFilterSchemas();
+    method public java.util.List getInformationalRankingExpressions();
     method public androidx.appsearch.app.JoinSpec? getJoinSpec();
     method public int getMaxSnippetSize();
     method public int getOrder();
@@ -540,18 +623,26 @@
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
     method public int getResultGroupingTypeFlags();
+    method public java.util.List getSearchEmbeddings();
+    method public String? getSearchSourceLogTag();
     method public int getSnippetCount();
     method public int getSnippetCountPerProperty();
     method public int getTermMatch();
+    method public boolean isEmbeddingSearchEnabled();
+    method public boolean isListFilterHasPropertyFunctionEnabled();
     method public boolean isListFilterQueryLanguageEnabled();
+    method public boolean isListFilterTokenizeFunctionEnabled();
     method public boolean isNumericSearchEnabled();
     method public boolean isVerbatimSearchEnabled();
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_COSINE = 1; // 0x1
+    field public static final int EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT = 2; // 0x2
+    field 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 @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA) public static final int GROUPING_TYPE_PER_SCHEMA = 4; // 0x4
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
-    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
+    field @Deprecated public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; // 0x9
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
@@ -562,6 +653,7 @@
     field public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; // 0x7
     field public static final int RANKING_STRATEGY_USAGE_COUNT = 4; // 0x4
     field public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; // 0x5
+    field public static final String SCHEMA_TYPE_WILDCARD = "*";
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -574,15 +666,27 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterNamespaces(java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterPackageNames(java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterProperties(String, java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSpec.Builder addFilterPropertyPaths(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.lang.String!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) public androidx.appsearch.app.SearchSpec.Builder addInformationalRankingExpressions(java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(androidx.appsearch.app.EmbeddingVector!...);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder addSearchEmbeddings(java.util.Collection);
     method public androidx.appsearch.app.SearchSpec build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setDefaultEmbeddingSearchMetricType(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) public androidx.appsearch.app.SearchSpec.Builder setEmbeddingSearchEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterHasPropertyFunctionEnabled(boolean);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_QUERY_LANGUAGE) public androidx.appsearch.app.SearchSpec.Builder setListFilterQueryLanguageEnabled(boolean);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.LIST_FILTER_TOKENIZE_FUNCTION) public androidx.appsearch.app.SearchSpec.Builder setListFilterTokenizeFunctionEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.NUMERIC_SEARCH) public androidx.appsearch.app.SearchSpec.Builder setNumericSearchEnabled(boolean);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
@@ -594,6 +698,7 @@
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(String);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) public androidx.appsearch.app.SearchSpec.Builder setSearchSourceLogTag(String);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCount(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setSnippetCountPerProperty(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setTermMatch(int);
@@ -613,6 +718,7 @@
   public final class SearchSuggestionSpec {
     method public java.util.Map!> getFilterDocumentIds();
     method public java.util.List getFilterNamespaces();
+    method public java.util.Map!> getFilterProperties();
     method public java.util.List getFilterSchemas();
     method public int getMaximumResultCount();
     method public int getRankingStrategy();
@@ -629,6 +735,10 @@
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterDocumentIds(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterNamespaces(java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterProperties(String, java.util.Collection);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(Class, java.util.Collection) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterPropertyPaths(String, java.util.Collection);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.lang.String!...);
     method public androidx.appsearch.app.SearchSuggestionSpec.Builder addFilterSchemas(java.util.Collection);
     method public androidx.appsearch.app.SearchSuggestionSpec build();
@@ -637,9 +747,11 @@
 
   public final class SetSchemaRequest {
     method public java.util.Map getMigrators();
+    method public java.util.Map getPubliclyVisibleSchemas();
     method public java.util.Map!>!> getRequiredPermissionsForSchemaTypeVisibility();
     method public java.util.Set getSchemas();
     method public java.util.Set getSchemasNotDisplayedBySystem();
+    method public java.util.Map!> getSchemasVisibleToConfigs();
     method public java.util.Map!> getSchemasVisibleToPackages();
     method @IntRange(from=1) public int getVersion();
     method public boolean isForceOverride();
@@ -653,26 +765,32 @@
 
   public static final class SetSchemaRequest.Builder {
     ctor public SetSchemaRequest.Builder();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClassVisibleToConfig(Class, androidx.appsearch.app.SchemaVisibilityConfig) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(Class!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder addDocumentClasses(java.util.Collection!>) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForDocumentClassVisibility(Class, java.util.Set) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder addRequiredPermissionsForSchemaTypeVisibility(String, java.util.Set);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder addSchemaTypeVisibleToConfig(String, androidx.appsearch.app.SchemaVisibilityConfig);
     method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(androidx.appsearch.app.AppSearchSchema!...);
     method public androidx.appsearch.app.SetSchemaRequest.Builder addSchemas(java.util.Collection);
     method public androidx.appsearch.app.SetSchemaRequest build();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearDocumentClassVisibleToConfigs(Class) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForDocumentClassVisibility(Class) throws androidx.appsearch.exceptions.AppSearchException;
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) public androidx.appsearch.app.SetSchemaRequest.Builder clearRequiredPermissionsForSchemaTypeVisibility(String);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) public androidx.appsearch.app.SetSchemaRequest.Builder clearSchemaTypeVisibleToConfigs(String);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassDisplayedBySystem(Class, boolean) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setDocumentClassVisibilityForPackage(Class, boolean, androidx.appsearch.app.PackageIdentifier) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SetSchemaRequest.Builder setForceOverride(boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrator(String, androidx.appsearch.app.Migrator);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setMigrators(java.util.Map);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleDocumentClass(Class, androidx.appsearch.app.PackageIdentifier?) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) public androidx.appsearch.app.SetSchemaRequest.Builder setPubliclyVisibleSchema(String, androidx.appsearch.app.PackageIdentifier?);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeDisplayedBySystem(String, boolean);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setSchemaTypeVisibilityForPackage(String, boolean, androidx.appsearch.app.PackageIdentifier);
     method public androidx.appsearch.app.SetSchemaRequest.Builder setVersion(@IntRange(from=1) int);
   }
 
-  public class SetSchemaResponse {
+  public final class SetSchemaResponse {
     method public java.util.Set getDeletedTypes();
     method public java.util.Set getIncompatibleTypes();
     method public java.util.Set getMigratedTypes();
@@ -700,7 +818,7 @@
     method public String getSchemaType();
   }
 
-  public class StorageInfo {
+  public final class StorageInfo {
     method public int getAliveDocumentsCount();
     method public int getAliveNamespacesCount();
     method public long getSizeBytes();
@@ -771,6 +889,51 @@
 
 }
 
+package androidx.appsearch.usagereporting {
+
+  @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) @androidx.appsearch.annotation.Document(name="builtin:ClickAction") public class ClickAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public String? getQuery();
+    method public String? getReferencedQualifiedId();
+    method public int getResultRankGlobal();
+    method public int getResultRankInBlock();
+    method public long getTimeStayOnResultMillis();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class ClickAction.Builder {
+    ctor public ClickAction.Builder(androidx.appsearch.usagereporting.ClickAction);
+    ctor public ClickAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.ClickAction build();
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setQuery(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setReferencedQualifiedId(String?);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankGlobal(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setResultRankInBlock(int);
+    method public androidx.appsearch.usagereporting.ClickAction.Builder setTimeStayOnResultMillis(long);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:SearchAction") public class SearchAction extends androidx.appsearch.usagereporting.TakenAction {
+    method public int getFetchedResultCount();
+    method public String? getQuery();
+  }
+
+  @androidx.appsearch.annotation.Document.BuilderProducer public static final class SearchAction.Builder {
+    ctor public SearchAction.Builder(androidx.appsearch.usagereporting.SearchAction);
+    ctor public SearchAction.Builder(String, String, long);
+    method public androidx.appsearch.usagereporting.SearchAction build();
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setDocumentTtlMillis(long);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setFetchedResultCount(int);
+    method public androidx.appsearch.usagereporting.SearchAction.Builder setQuery(String?);
+  }
+
+  @androidx.appsearch.annotation.Document(name="builtin:TakenAction") public abstract class TakenAction {
+    method public long getActionTimestampMillis();
+    method public long getDocumentTtlMillis();
+    method public String getId();
+    method public String getNamespace();
+  }
+
+}
+
 package androidx.appsearch.util {
 
   public class DocumentIdUtil {
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 0aa30ef..32b0b69 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -47,6 +47,8 @@
     implementation('androidx.concurrent:concurrent-futures:1.0.0')
     implementation("androidx.core:core:1.6.0")
 
+    annotationProcessor project(':appsearch:appsearch-compiler')
+
     androidTestAnnotationProcessor project(':appsearch:appsearch-compiler')
     androidTestImplementation project(':appsearch:appsearch-builtin-types')
     androidTestImplementation project(':appsearch:appsearch-local-storage')
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index 5d52268..7d18f74 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -24,6 +24,7 @@
 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
 import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
+import static androidx.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -2764,4 +2765,160 @@
         assertThat(person.getFirstName()).isEqualTo("first");
         assertThat(person.getLastName()).isEqualTo("last");
     }
+
+    @Document
+    static class EmailWithEmbedding {
+        @Document.Namespace
+        String mNamespace;
+
+        @Document.Id
+        String mId;
+
+        @Document.CreationTimestampMillis
+        long mCreationTimestampMillis;
+
+        @Document.StringProperty
+        String mSender;
+
+        // Default non-indexable embedding
+        @Document.EmbeddingProperty
+        EmbeddingVector mSenderEmbedding;
+
+        @Document.EmbeddingProperty(indexingType = 1)
+        EmbeddingVector mTitleEmbedding;
+
+        @Document.EmbeddingProperty(indexingType = 1)
+        Collection mReceiverEmbeddings;
+
+        @Document.EmbeddingProperty(indexingType = 1)
+        EmbeddingVector[] mBodyEmbeddings;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            EmailWithEmbedding email = (EmailWithEmbedding) o;
+            return Objects.equals(mNamespace, email.mNamespace) && Objects.equals(mId,
+                    email.mId) && Objects.equals(mSender, email.mSender)
+                    && Objects.equals(mSenderEmbedding, email.mSenderEmbedding)
+                    && Objects.equals(mTitleEmbedding, email.mTitleEmbedding)
+                    && Objects.equals(mReceiverEmbeddings, email.mReceiverEmbeddings)
+                    && Arrays.equals(mBodyEmbeddings, email.mBodyEmbeddings);
+        }
+
+        public static EmailWithEmbedding createSampleDoc() {
+            EmbeddingVector embedding1 =
+                    new EmbeddingVector(new float[]{1, 2, 3}, "model1");
+            EmbeddingVector embedding2 =
+                    new EmbeddingVector(new float[]{-1, -2, -3}, "model2");
+            EmbeddingVector embedding3 =
+                    new EmbeddingVector(new float[]{0.1f, 0.2f, 0.3f, 0.4f}, "model3");
+            EmbeddingVector embedding4 =
+                    new EmbeddingVector(new float[]{-0.1f, -0.2f, -0.3f, -0.4f}, "model3");
+            EmailWithEmbedding email = new EmailWithEmbedding();
+            email.mNamespace = "namespace";
+            email.mId = "id";
+            email.mCreationTimestampMillis = 1000;
+            email.mSender = "sender";
+            email.mSenderEmbedding = embedding1;
+            email.mTitleEmbedding = embedding2;
+            email.mReceiverEmbeddings = Collections.singletonList(embedding3);
+            email.mBodyEmbeddings = new EmbeddingVector[]{embedding3, embedding4};
+            return email;
+        }
+    }
+
+    @Test
+    public void testEmbeddingGenericDocumentConversion() throws Exception {
+        EmailWithEmbedding inEmail = EmailWithEmbedding.createSampleDoc();
+        GenericDocument genericDocument1 = GenericDocument.fromDocumentClass(inEmail);
+        GenericDocument genericDocument2 = GenericDocument.fromDocumentClass(inEmail);
+        EmailWithEmbedding outEmail = genericDocument2.toDocumentClass(EmailWithEmbedding.class);
+
+        assertThat(inEmail).isNotSameInstanceAs(outEmail);
+        assertThat(inEmail).isEqualTo(outEmail);
+        assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2);
+        assertThat(genericDocument1).isEqualTo(genericDocument2);
+    }
+
+    @Test
+    public void testEmbeddingSearch() throws Exception {
+        assumeTrue(mSession.getFeatures().isFeatureSupported(
+                Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        mSession.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addDocumentClasses(EmailWithEmbedding.class)
+                .build()).get();
+
+        // Create and add a document
+        EmailWithEmbedding email = EmailWithEmbedding.createSampleDoc();
+        checkIsBatchResultSuccess(mSession.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addDocuments(email)
+                        .build()));
+
+        // An empty query should retrieve this document.
+        SearchResults searchResults = mSession.search("",
+                new SearchSpec.Builder().build());
+        List results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(1);
+        // Convert GenericDocument to EmailWithEmbedding and check values.
+        EmailWithEmbedding outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+        assertThat(outputDocument).isEqualTo(email);
+
+        // senderEmbedding is non-indexable, so querying for it will return nothing.
+        searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), 0.9, 1)",
+                new SearchSpec.Builder()
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+                        .addSearchEmbeddings(email.mSenderEmbedding)
+                        .setRankingStrategy(
+                                "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build());
+        results = retrieveAllSearchResults(searchResults);
+        assertThat(results).isEmpty();
+
+        // titleEmbedding is indexable, and querying for it using itself will return a cosine
+        // similarity score of 1.
+        searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), 0.9, 1)",
+                new SearchSpec.Builder()
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+                        .addSearchEmbeddings(email.mTitleEmbedding)
+                        .setRankingStrategy(
+                                "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build());
+        results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(1);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(1);
+        // Convert GenericDocument to EmailWithEmbedding and check values.
+        outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+        assertThat(outputDocument).isEqualTo(email);
+
+        // Both receiverEmbeddings and bodyEmbeddings are indexable, and in this specific
+        // document, they together hold three embedding vectors with the same signature.
+        searchResults = mSession.search("semanticSearch(getSearchSpecEmbedding(0), -1, 1)",
+                new SearchSpec.Builder()
+                        .setDefaultEmbeddingSearchMetricType(
+                                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
+                        // Using one of the three vectors to query
+                        .addSearchEmbeddings(email.mBodyEmbeddings[0])
+                        .setRankingStrategy(
+                                // We should get a score of 3 for "len", since there are three
+                                // embedding vectors matched.
+                                "len(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                        .setListFilterQueryLanguageEnabled(true)
+                        .setEmbeddingSearchEnabled(true)
+                        .build());
+        results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(1);
+        assertThat(results.get(0).getRankingSignal()).isEqualTo(3);
+        // Convert GenericDocument to EmailWithEmbedding and check values.
+        outputDocument = results.get(0).getDocument(EmailWithEmbedding.class);
+        assertThat(outputDocument).isEqualTo(email);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSchemaInternalTest.java
deleted file mode 100644
index 686c44f..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/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 androidx.appsearch.app;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.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 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/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
index c984f92..4307c12 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -21,16 +21,12 @@
 
 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 androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.testutil.AppSearchEmail;
-import androidx.appsearch.util.DocumentIdUtil;
-import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -46,6 +42,8 @@
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 
+
+/** This class holds all tests that won't be exported to the framework.  */
 public abstract class AppSearchSessionInternalTestBase {
 
     static final String DB_NAME_1 = "";
@@ -77,730 +75,10 @@
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().setForceOverride(true).build()).get();
     }
 
-    // TODO(b/228240987) delete this test when we support property restrict for multiple terms
-    @Test
-    public void testSearchSuggestion_propertyFilter() throws Exception {
-        // Schema registration
-        AppSearchSchema schemaType1 =
-                new AppSearchSchema.Builder("Type1")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertyone")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertytwo")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        AppSearchSchema schemaType2 =
-                new AppSearchSchema.Builder("Type2")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertythree")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .addProperty(
-                                new StringPropertyConfig.Builder("propertyfour")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
-                .get();
-
-        // Index documents
-        GenericDocument doc1 =
-                new GenericDocument.Builder<>("namespace", "id1", "Type1")
-                        .setPropertyString("propertyone", "termone")
-                        .setPropertyString("propertytwo", "termtwo")
-                        .build();
-        GenericDocument doc2 =
-                new GenericDocument.Builder<>("namespace", "id2", "Type2")
-                        .setPropertyString("propertythree", "termthree")
-                        .setPropertyString("propertyfour", "termfour")
-                        .build();
-
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
-
-        SearchSuggestionResult resultOne =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
-        SearchSuggestionResult resultTwo =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
-        SearchSuggestionResult resultThree =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
-        SearchSuggestionResult resultFour =
-                new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
-
-        // Only search for type1/propertyone
-        List suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1")
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne);
-
-        // Only search for type1/propertyone and type1/propertytwo
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1")
-                                        .addFilterProperties(
-                                                "Type1",
-                                                ImmutableList.of("propertyone", "propertytwo"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultTwo);
-
-        // Only search for type1/propertyone and type2/propertythree
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1", "Type2")
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .addFilterProperties(
-                                                "Type2", ImmutableList.of("propertythree"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultThree);
-
-        // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterSchemas("Type1", "Type2")
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .addFilterPropertyPaths(
-                                                "Type2",
-                                                ImmutableList.of(new PropertyPath("propertyfour")))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultFour);
-
-        // Only search for type1/propertyone and everything in type2
-        suggestions =
-                mDb1.searchSuggestionAsync(
-                                /* suggestionQueryExpression= */ "t",
-                                new SearchSuggestionSpec.Builder(/* totalResultCount= */ 10)
-                                        .addFilterProperties(
-                                                "Type1", ImmutableList.of("propertyone"))
-                                        .build())
-                        .get();
-        assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFilters() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email1 =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        AppSearchEmail email2 =
-                new AppSearchEmail.Builder("namespace", "id2")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email1, email2).build()));
-
-        // Query with type property filters {"Email", ["subject", "to"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
-                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        // Only email2 should be returned because email1 doesn't have the term "body" in subject
-        // or to fields
-        assertThat(documents).containsExactly(email2);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
-        // schema has body in its property filter but Email schema doesn't.
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
-                .addFilterProperties("Note", ImmutableList.of("body"))
-                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        // Only the note document should be returned because the email property filter doesn't
-        // allow searching in the body.
-        assertThat(documents).containsExactly(note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"*": ["subject", "title"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
-                        ImmutableList.of("subject", "title"))
-                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        // The wildcard property filter will apply to both the Email and Note schema. The email
-        // document should be returned since it has the term "body" in its subject property. The
-        // note document should not be returned since it doesn't have the term "body" in the title
-        // property (subject property is not applicable for Note schema)
-        assertThat(documents).containsExactly(email);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
-                        ImmutableList.of("subject", "title"))
-                .addFilterProperties("Note", ImmutableList.of("body"))
-                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        // The wildcard property filter will only apply to the Email schema since Note schema has
-        // its own explicit property filter specified. The email document should be returned since
-        // it has the term "body" in its subject property. The note document should also be returned
-        // since it has the term "body" in the body property.
-        assertThat(documents).containsExactly(email, note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example subject with some body")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"NonExistentType": ["to", "title"]}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
-                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        // The supplied property filters don't apply to either schema types. Both the documents
-        // should be returned since the term "body" is present in at least one of their properties.
-        assertThat(documents).containsExactly(email, note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersEmpty() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .addSchemas(new AppSearchSchema.Builder("Note")
-                                .addProperty(new StringPropertyConfig.Builder("title")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .addProperty(new StringPropertyConfig.Builder("body")
-                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                                        .setTokenizerType(
-                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .build())
-                                .build())
-                        .build()).get();
-
-        // Index two documents
-        AppSearchEmail email =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setCreationTimestampMillis(1000)
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        GenericDocument note =
-                new GenericDocument.Builder<>("namespace", "id2", "Note")
-                        .setCreationTimestampMillis(1000)
-                        .setPropertyString("title", "Note title")
-                        .setPropertyString("body", "Note body").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder()
-                        .addGenericDocuments(email, note).build()));
-
-        // Query with type property paths {"email": []}
-        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
-                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        // The email document should not be returned since the property filter doesn't allow
-        // searching any property.
-        assertThat(documents).containsExactly(note);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        assumeTrue(mDb1.getFeatures()
-                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-
-        // A full example of how join might be used with property filters in join spec
-        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
-                .addProperty(new StringPropertyConfig.Builder("entityId")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setJoinableValueType(StringPropertyConfig
-                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("note")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("viewType")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
-                        .build()).get();
-
-        // Index 2 email documents
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        // Index 2 viewAction documents, one for email1 and the other for email2
-        String qualifiedId1 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id1");
-        String qualifiedId2 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id2");
-        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
-                .setPropertyString("entityId", qualifiedId1)
-                .setPropertyString("note", "Viewed email on Monday")
-                .setPropertyString("viewType", "Stared").build();
-        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
-                .setPropertyString("entityId", qualifiedId2)
-                .setPropertyString("note", "Viewed email on Tuesday")
-                .setPropertyString("viewType", "Viewed").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
-                                viewAction1, viewAction2)
-                        .build()));
-
-        // The nested search spec only allows searching the viewType property for viewAction
-        // schema type. It also specifies a property filter for Email schema.
-        SearchSpec nestedSearchSpec =
-                new SearchSpec.Builder()
-                        .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
-                        .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
-                                ImmutableList.of("subject"))
-                        .build();
-
-        // Search for the term "Viewed" in join spec
-        JoinSpec js = new JoinSpec.Builder("entityId")
-                .setNestedSearch("Viewed", nestedSearchSpec)
-                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
-                .build();
-
-        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
-                .setJoinSpec(js)
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .build());
-
-        List sr = searchResults.getNextPageAsync().get();
-
-        // Both email docs are returned, email2 comes first because it has higher number of
-        // joined documents. The property filters for Email schema specified in the nested search
-        // specs don't apply to the outer query (otherwise none of the email documents would have
-        // been returned).
-        assertThat(sr).hasSize(2);
-
-        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
-        // the join spec, so it should be present in the joined results.
-        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
-        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
-        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
-        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
-
-        // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
-        // in the join spec, so it should not be present in the joined results.
-        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
-        assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
-        assertThat(sr.get(1).getJoinedResults()).isEmpty();
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
-        assumeTrue(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        assumeTrue(mDb1.getFeatures()
-                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
-
-        // A full example of how join might be used with property filters in join spec
-        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
-                .addProperty(new StringPropertyConfig.Builder("entityId")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setJoinableValueType(StringPropertyConfig
-                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("note")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).addProperty(new StringPropertyConfig.Builder("viewType")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build()
-                ).build();
-
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
-                        .build()).get();
-
-        // Index 2 email documents
-        AppSearchEmail inEmail =
-                new AppSearchEmail.Builder("namespace", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-
-        // Index 2 viewAction documents, one for email1 and the other for email2
-        String qualifiedId1 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id1");
-        String qualifiedId2 =
-                DocumentIdUtil.createQualifiedId(
-                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
-                        "namespace", "id2");
-        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
-                .setPropertyString("entityId", qualifiedId1)
-                .setPropertyString("note", "Viewed email on Monday")
-                .setPropertyString("viewType", "Stared").build();
-        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
-                .setPropertyString("entityId", qualifiedId2)
-                .setPropertyString("note", "Viewed email on Tuesday")
-                .setPropertyString("viewType", "Viewed").build();
-        checkIsBatchResultSuccess(mDb1.putAsync(
-                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
-                                viewAction1, viewAction2)
-                        .build()));
-
-        // The nested search spec doesn't specify any property filters.
-        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
-
-        // Search for the term "Viewed" in join spec
-        JoinSpec js = new JoinSpec.Builder("entityId")
-                .setNestedSearch("Viewed", nestedSearchSpec)
-                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
-                .build();
-
-        // Outer search spec adds property filters for both Email and ViewAction schema
-        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
-                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
-                .setJoinSpec(js)
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
-                .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
-                .build());
-
-        List sr = searchResults.getNextPageAsync().get();
-
-        // Both email docs are returned as they both satisfy the property filters for Email, email2
-        // comes first because it has higher id lexicographically.
-        assertThat(sr).hasSize(2);
-
-        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
-        // the outer spec (although those property filters are irrelevant for joined documents),
-        // it should be present in the joined results.
-        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
-        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
-        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
-        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
-
-        // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
-        // in the outer spec, but property filters in the outer spec should not apply on joined
-        // documents, so viewAction1 should be present in the joined results.
-        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
-        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
-        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
-        assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
-    }
-
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_typePropertyFiltersNotSupported() throws Exception {
-        assumeFalse(mDb1.getFeatures().isFeatureSupported(
-                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                new SetSchemaRequest.Builder()
-                        .addSchemas(AppSearchEmail.SCHEMA)
-                        .build()).get();
-
-        // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
-        // exception is thrown
-        SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
-                .build();
-        UnsupportedOperationException exception =
-                assertThrows(UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec));
-        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
-                + " is not available on this AppSearch implementation.");
-    }
-
     // TODO(b/268521214): Move test to cts once deletion propagation is available in framework.
     @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(
@@ -820,11 +98,6 @@
                                         .setJoinableValueType(
                                                 StringPropertyConfig
                                                         .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        // TODO(b/274157614): Export this to framework when we
-                                        //  can access hidden APIs.
-                                        // @exportToFramework:startStrip()
-                                        .setDeletionPropagation(true)
-                                        // @exportToFramework:endStrip()
                                         .build())
                         .build();
 
@@ -837,465 +110,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.");
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
-        assumeTrue(
-                mDb1.getFeatures()
-                        .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
-        // Schema registration
-        AppSearchSchema genericSchema =
-                new AppSearchSchema.Builder("Generic")
-                        .addProperty(
-                                new StringPropertyConfig.Builder("foo")
-                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                                        .setIndexingType(
-                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                                        .build())
-                        .build();
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder()
-                                .addSchemas(AppSearchEmail.SCHEMA)
-                                .addSchemas(genericSchema)
-                                .build())
-                .get();
-
-        // Index four documents.
-        AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("namespace1", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace1", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
-        AppSearchEmail inEmail3 =
-                new AppSearchEmail.Builder("namespace2", "id3")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
-        AppSearchEmail inEmail4 =
-                new AppSearchEmail.Builder("namespace2", "id4")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
-        AppSearchEmail inEmail5 =
-                new AppSearchEmail.Builder("namespace2", "id5")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
-        GenericDocument inDoc1 =
-                new GenericDocument.Builder<>("namespace3", "id6", "Generic")
-                        .setPropertyString("foo", "body")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
-        GenericDocument inDoc2 =
-                new GenericDocument.Builder<>("namespace3", "id7", "Generic")
-                        .setPropertyString("foo", "body")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
-        GenericDocument inDoc3 =
-                new GenericDocument.Builder<>("namespace4", "id8", "Generic")
-                        .setPropertyString("foo", "body")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
-
-        // Query with per package result grouping. Only the last document 'doc3' should be
-        // returned.
-        SearchResults searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* resultLimit= */ 1)
-                                .build());
-        List documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3);
-
-        // Query with per namespace result grouping. Only the last document in each namespace should
-        // be returned ('doc3', 'doc2', 'email5' and 'email2').
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per namespace result grouping. Two of the last documents in each namespace
-        // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
-        // Query with per schema result grouping. Only the last document of each schema type should
-        // be returned ('doc3', 'email5')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inEmail5);
-
-        // Query with per schema result grouping. Only the last two documents of each schema type
-        // should be returned ('doc3', 'doc2', 'email5', 'email4')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
-
-        // Query with per package and per namespace result grouping. Only the last document in each
-        // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per package and per namespace result grouping. Only the last two documents
-        // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
-        // 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
-        // Query with per package and per schema type result grouping. Only the last document in
-        // each schema type should be returned. ('doc3', 'email5')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inEmail5);
-
-        // Query with per package and per schema type result grouping. Only the last two document in
-        // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
-
-        // Query with per namespace and per schema type result grouping. Only the last document in
-        // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per namespace and per schema type result grouping. Only the last two documents
-        // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
-        // 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-
-        // Query with per namespace, per package and per schema type result grouping. Only the last
-        // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 1)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
-
-        // Query with per namespace, per package and per schema type result grouping. Only the last
-        // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
-        // 'email4', 'email2', 'email1')
-        searchResults =
-                mDb1.search(
-                        "body",
-                        new SearchSpec.Builder()
-                                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                                .setResultGrouping(
-                                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                                | SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                                | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                        /* resultLimit= */ 2)
-                                .build());
-        documents = convertSearchResultsToDocuments(searchResults);
-        assertThat(documents)
-                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
-    }
-
-    // TODO(b/291122592): move to CTS once the APIs it uses are public
-    @Test
-    public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
-        assumeFalse(
-                mDb1.getFeatures()
-                        .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
-        // Schema registration
-        mDb1.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
-                .get();
-
-        // Index four documents.
-        AppSearchEmail inEmail1 =
-                new AppSearchEmail.Builder("namespace1", "id1")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
-        AppSearchEmail inEmail2 =
-                new AppSearchEmail.Builder("namespace1", "id2")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
-        AppSearchEmail inEmail3 =
-                new AppSearchEmail.Builder("namespace2", "id3")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
-        AppSearchEmail inEmail4 =
-                new AppSearchEmail.Builder("namespace2", "id4")
-                        .setFrom("[email protected]")
-                        .setTo("[email protected]", "[email protected]")
-                        .setSubject("testPut example")
-                        .setBody("This is the body of the testPut email")
-                        .build();
-        checkIsBatchResultSuccess(
-                mDb1.putAsync(
-                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec1 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* resultLimit= */ 1)
-                        .build();
-        UnsupportedOperationException exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec1));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec2 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_PACKAGE
-                                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                /* resultLimit= */ 1)
-                        .build();
-        exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec2));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec3 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
-                                /* resultLimit= */ 1)
-                        .build();
-        exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec3));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-
-        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
-        // UnsupportedOperationException will be thrown.
-        SearchSpec searchSpec4 =
-                new SearchSpec.Builder()
-                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                        .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA
-                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
-                                /* resultLimit= */ 1)
-                        .build();
-        exception =
-                assertThrows(
-                        UnsupportedOperationException.class,
-                        () -> mDb1.search("body", searchSpec4));
-        assertThat(exception)
-                .hasMessageThat()
-                .contains(
-                        Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
-                                + " is not available on this"
-                                + " AppSearch implementation.");
-    }
-
     @Test
     public void testQuery_typeFilterWithPolymorphism() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
@@ -1664,4 +478,167 @@
         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()));
+
+        SearchResults 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 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
+        SearchResults searchResults = mDb1.search("Network", new SearchSpec.Builder()
+                .addFilterSchemas("Message")
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("carrier"))
+                .build());
+        List 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/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
index 6bcc933..718aaa2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionLocalInternalTest.java
@@ -26,7 +26,6 @@
 
 import java.util.concurrent.ExecutorService;
 
-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API.
 public class AppSearchSessionLocalInternalTest extends AppSearchSessionInternalTestBase {
     @Override
     protected ListenableFuture createSearchSessionAsync(@NonNull String dbName) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
index 4165efe..76f32b4 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
@@ -26,11 +26,8 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import org.junit.Test;
-
 import java.util.concurrent.ExecutorService;
 
-// TODO(b/227356108): move this test to cts test once we un-hide search suggestion API.
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class AppSearchSessionPlatformInternalTest extends AppSearchSessionInternalTestBase {
     @Override
@@ -48,10 +45,4 @@
                 new PlatformStorage.SearchContext.Builder(context, dbName)
                         .setWorkerExecutor(executor).build());
     }
-
-    @Override
-    @Test
-    public void testSearchSuggestion_propertyFilter() throws Exception {
-        // TODO(b/227356108) enable the test when suggestion is ready in platform.
-    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
index 7859463..fc5be8f 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/GenericDocumentInternalTest.java
@@ -18,9 +18,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.os.Bundle;
 import android.os.Parcel;
 
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -44,7 +45,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeBundle(inDoc.getBundle());
+        inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
@@ -52,11 +53,13 @@
         Parcel outParcel = Parcel.obtain();
         outParcel.unmarshall(data, 0, data.length);
         outParcel.setDataPosition(0);
-        Bundle outBundle = outParcel.readBundle();
+        @SuppressWarnings("deprecation")
+        GenericDocumentParcel documentParcel =
+                outParcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
         outParcel.recycle();
 
         // Compare results
-        GenericDocument outDoc = new GenericDocument(outBundle);
+        GenericDocument outDoc = new GenericDocument(documentParcel);
         assertThat(inDoc).isEqualTo(outDoc);
         assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
         assertThat(outDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
@@ -83,7 +86,7 @@
 
         // Serialize the document
         Parcel inParcel = Parcel.obtain();
-        inParcel.writeBundle(inDoc.getBundle());
+        inParcel.writeParcelable(inDoc.getDocumentParcel(), /*parcelableFlags=*/ 0);
         byte[] data = inParcel.marshall();
         inParcel.recycle();
 
@@ -91,11 +94,13 @@
         Parcel outParcel = Parcel.obtain();
         outParcel.unmarshall(data, 0, data.length);
         outParcel.setDataPosition(0);
-        Bundle outBundle = outParcel.readBundle();
+        @SuppressWarnings("deprecation")
+        GenericDocumentParcel documentParcel =
+                outParcel.readParcelable(GenericDocumentParcel.class.getClassLoader());
         outParcel.recycle();
 
         // Compare results
-        GenericDocument outDoc = new GenericDocument(outBundle);
+        GenericDocument outDoc = new GenericDocument(documentParcel);
         assertThat(inDoc).isEqualTo(outDoc);
         assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
         assertThat(outDoc.getPropertyString("propString")).isEqualTo("Hello");
@@ -112,47 +117,23 @@
                 .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
                 .setScore(42)
                 .setPropertyString("propString", "Hello")
-                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
-                .setPropertyDocument(
-                        "propDocument",
-                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
-                                .setPropertyString("propString", "Goodbye")
-                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
-                                .build())
                 .build();
 
         GenericDocument newDoc = new GenericDocument.Builder<>(oldDoc)
                 .setParentTypes(new ArrayList<>(Arrays.asList("Class3", "Class4")))
-                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
-                .setPropertyDocument(
-                        "propDocument",
-                        new GenericDocument.Builder<>("namespace", "id3", "schema3")
-                                .setPropertyString("propString", "Bye")
-                                .setPropertyBytes("propBytes", new byte[][]{{5, 6}})
-                                .build())
+                .setPropertyString("propString", "Bye")
                 .build();
 
         // Check that the original GenericDocument is unmodified.
         assertThat(oldDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
         assertThat(oldDoc.getScore()).isEqualTo(42);
         assertThat(oldDoc.getPropertyString("propString")).isEqualTo("Hello");
-        assertThat(oldDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
-        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
-                .isEqualTo("Goodbye");
-        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
-                .isEqualTo(new byte[][]{{3, 4}});
 
         // Check that the new GenericDocument has modified the original fields correctly.
         assertThat(newDoc.getParentTypes()).isEqualTo(Arrays.asList("Class3", "Class4"));
-        assertThat(newDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
-        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
-                .isEqualTo("Bye");
-        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
-                .isEqualTo(new byte[][]{{5, 6}});
+        assertThat(newDoc.getPropertyString("propString")).isEqualTo("Bye");
 
         // Check that the new GenericDocument copies fields that aren't set.
         assertThat(oldDoc.getScore()).isEqualTo(newDoc.getScore());
-        assertThat(oldDoc.getPropertyString("propString")).isEqualTo(newDoc.getPropertyString(
-                "propString"));
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java
new file mode 100644
index 0000000..c63e53e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/InternalVisibilityConfigTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class InternalVisibilityConfigTest {
+
+    @Test
+    public void testVisibilityConfig_setNotDisplayBySystem() {
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
+                .setNotDisplayedBySystem(true).build();
+
+        assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();
+    }
+
+    @Test
+    public void testVisibilityConfig_setVisibilityConfig() {
+        String visibleToPackage1 = "com.example.package";
+        byte[] visibleToPackageCert1 = new byte[32];
+        String visibleToPackage2 = "com.example.package2";
+        byte[] visibleToPackageCert2 = new byte[32];
+
+        SchemaVisibilityConfig innerConfig1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(
+                        new PackageIdentifier(visibleToPackage1, visibleToPackageCert1))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SchemaVisibilityConfig innerConfig2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(
+                        new PackageIdentifier(visibleToPackage2, visibleToPackageCert2))
+                .addRequiredPermissions(ImmutableSet.of(3, 4))
+                .build();
+
+        InternalVisibilityConfig visibilityConfig = new InternalVisibilityConfig.Builder("schema")
+                .addVisibleToConfig(innerConfig1)
+                .addVisibleToConfig(innerConfig2)
+                .build();
+
+        assertThat(visibilityConfig.getVisibleToConfigs())
+                .containsExactly(innerConfig1, innerConfig2);
+    }
+
+    @Test
+    public void testToInternalVisibilityConfig() {
+        byte[] packageSha256Cert = new byte[32];
+        packageSha256Cert[0] = 24;
+        packageSha256Cert[8] = 23;
+        packageSha256Cert[16] = 22;
+        packageSha256Cert[24] = 21;
+
+        // Create a SetSchemaRequest for testing
+        SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("testSchema").build())
+                .setSchemaTypeDisplayedBySystem("testSchema", false)
+                .setSchemaTypeVisibilityForPackage("testSchema", /*visible=*/true,
+                        new PackageIdentifier("com.example.test", packageSha256Cert))
+                .setPubliclyVisibleSchema("testSchema",
+                        new PackageIdentifier("com.example.test1", packageSha256Cert))
+                .build();
+
+        // Convert the SetSchemaRequest to GenericDocument map
+        List visibilityConfigs =
+                InternalVisibilityConfig.toInternalVisibilityConfigs(setSchemaRequest);
+
+        // Check if the conversion is correct
+        assertThat(visibilityConfigs).hasSize(1);
+        InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(0);
+        assertThat(visibilityConfig.isNotDisplayedBySystem()).isTrue();
+        assertThat(visibilityConfig.getVisibilityConfig().getAllowedPackages())
+                .containsExactly(new PackageIdentifier("com.example.test", packageSha256Cert));
+        assertThat(visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage())
+                .isNotNull();
+        assertThat(
+                visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+                        .getPackageName())
+                .isEqualTo("com.example.test1");
+        assertThat(
+                visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage()
+                        .getSha256Certificate())
+                .isEqualTo(packageSha256Cert);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java
new file mode 100644
index 0000000..9f79469
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/JoinSpecInternalTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class JoinSpecInternalTest {
+    @Test
+    public void testJoinSpecBuilderCopyConstructor() {
+        JoinSpec joinSpec = new JoinSpec.Builder("childPropertyExpression")
+                .setMaxJoinedResultCount(10)
+                .setNestedSearch("nestedQuery", new SearchSpec.Builder().build())
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+        JoinSpec joinSpecCopy = new JoinSpec.Builder(joinSpec).build();
+        assertThat(joinSpecCopy.getMaxJoinedResultCount()).isEqualTo(
+                joinSpec.getMaxJoinedResultCount());
+        assertThat(joinSpecCopy.getChildPropertyExpression()).isEqualTo(
+                joinSpec.getChildPropertyExpression());
+        assertThat(joinSpecCopy.getNestedQuery()).isEqualTo(joinSpec.getNestedQuery());
+        assertThat(joinSpecCopy.getNestedSearchSpec()).isNotNull();
+        assertThat(joinSpecCopy.getAggregationScoringStrategy()).isEqualTo(
+                joinSpec.getAggregationScoringStrategy());
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
new file mode 100644
index 0000000..bec84cb
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultInternalTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SearchResultInternalTest {
+    @Test
+    public void testSearchResultBuilderCopyConstructor() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        SearchResult searchResult = new SearchResult.Builder("package", "database")
+                .setGenericDocument(document)
+                .setRankingSignal(1.23)
+                .addJoinedResult(new SearchResult.Builder("pkg1", "db1").setGenericDocument(
+                        document).build())
+                .addJoinedResult(new SearchResult.Builder("pkg2", "db2").setGenericDocument(
+                        document).build())
+                .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath1").build())
+                .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath2").build())
+                .addMatchInfo(new SearchResult.MatchInfo.Builder("propertyPath3").build())
+                .build();
+        SearchResult searchResultCopy = new SearchResult.Builder(searchResult).build();
+        assertThat(searchResultCopy.getGenericDocument()).isEqualTo(
+                searchResult.getGenericDocument());
+        assertThat(searchResultCopy.getRankingSignal()).isEqualTo(searchResult.getRankingSignal());
+        // Specifically test JoinedResults and MatchInfos with different sizes since briefly had
+        // a bug where we looped through joinedResults using matchInfos.size()
+        assertThat(searchResultCopy.getJoinedResults().size()).isEqualTo(
+                searchResult.getJoinedResults().size());
+        assertThat(searchResultCopy.getJoinedResults().get(0).getPackageName()).isEqualTo("pkg1");
+        assertThat(searchResultCopy.getJoinedResults().get(0).getDatabaseName()).isEqualTo("db1");
+        assertThat(searchResultCopy.getJoinedResults().get(1).getPackageName()).isEqualTo("pkg2");
+        assertThat(searchResultCopy.getJoinedResults().get(1).getDatabaseName()).isEqualTo("db2");
+        assertThat(searchResultCopy.getMatchInfos().size()).isEqualTo(
+                searchResult.getMatchInfos().size());
+        assertThat(searchResultCopy.getMatchInfos().get(0).getPropertyPath()).isEqualTo(
+                "propertyPath1");
+        assertThat(searchResultCopy.getMatchInfos().get(1).getPropertyPath()).isEqualTo(
+                "propertyPath2");
+        assertThat(searchResultCopy.getMatchInfos().get(2).getPropertyPath()).isEqualTo(
+                "propertyPath3");
+    }
+
+    @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();
+        SearchResult searchResult = new SearchResult.Builder("package", "database")
+                .setGenericDocument(document)
+                .addJoinedResult(new SearchResult.Builder("pkg", "db").setGenericDocument(
+                        document).build())
+                .clearJoinedResults()
+                .build();
+        assertThat(searchResult.getJoinedResults()).isEmpty();
+    }
+
+    @Test
+    public void testMatchInfoBuilderCopyConstructor() {
+        SearchResult.MatchRange exactMatchRange = new SearchResult.MatchRange(3, 8);
+        SearchResult.MatchRange submatchRange = new SearchResult.MatchRange(3, 5);
+        SearchResult.MatchRange snippetMatchRange = new SearchResult.MatchRange(1, 10);
+        SearchResult.MatchInfo matchInfo =
+                new SearchResult.MatchInfo.Builder("propertyPath1")
+                        .setExactMatchRange(exactMatchRange)
+                        .setSubmatchRange(submatchRange)
+                        .setSnippetRange(snippetMatchRange).build();
+        SearchResult.MatchInfo matchInfoCopy =
+                new SearchResult.MatchInfo.Builder(matchInfo).build();
+        assertThat(matchInfoCopy.getPropertyPath()).isEqualTo("propertyPath1");
+        assertThat(matchInfoCopy.getExactMatchRange()).isEqualTo(exactMatchRange);
+        assertThat(matchInfoCopy.getSubmatchRange()).isEqualTo(submatchRange);
+        assertThat(matchInfoCopy.getSnippetRange()).isEqualTo(snippetMatchRange);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java
new file mode 100644
index 0000000..e37d1f854
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchResultPageInternalTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SearchResultPageInternalTest {
+    @Test
+    public void testSearchResultPage() {
+        GenericDocument document =
+                new GenericDocument.Builder<>("namespace", "id", "schemaType").build();
+        List results = Arrays.asList(
+                new SearchResult.Builder("package1", "database1").setGenericDocument(
+                        document).build(),
+                new SearchResult.Builder("package2", "database2").setGenericDocument(
+                        document).build()
+        );
+        SearchResultPage searchResultPage = new SearchResultPage(/*nextPageToken=*/ 123, results);
+        assertThat(searchResultPage.getNextPageToken()).isEqualTo(123);
+        List searchResults = searchResultPage.getResults();
+        assertThat(searchResults).hasSize(2);
+        assertThat(searchResults.get(0).getPackageName()).isEqualTo("package1");
+        assertThat(searchResults.get(0).getDatabaseName()).isEqualTo("database1");
+        assertThat(searchResults.get(0).getGenericDocument()).isEqualTo(document);
+        assertThat(searchResults.get(1).getPackageName()).isEqualTo("package2");
+        assertThat(searchResults.get(1).getDatabaseName()).isEqualTo("database2");
+        assertThat(searchResults.get(1).getGenericDocument()).isEqualTo(document);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
index ed00e5a..e733fc3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecInternalTest.java
@@ -18,22 +18,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
-import android.os.Bundle;
-
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 
 import org.junit.Test;
 
-import java.util.List;
-import java.util.Map;
+import java.util.Arrays;
 
 /** Tests for private APIs of {@link SearchSpec}. */
 public class SearchSpecInternalTest {
 
     @Test
-    public void testGetBundle() {
+    public void testSearchSpecBuilder() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                 .addFilterNamespaces("namespace1", "namespace2")
@@ -50,28 +45,135 @@
                 .setListFilterQueryLanguageEnabled(true)
                 .build();
 
-        Bundle bundle = searchSpec.getBundle();
-        assertThat(bundle.getInt(SearchSpec.TERM_MATCH_TYPE_FIELD))
-                .isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
-        assertThat(bundle.getStringArrayList(SearchSpec.NAMESPACE_FIELD)).containsExactly(
+        assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+        assertThat(searchSpec.getFilterNamespaces()).containsExactly(
                 "namespace1", "namespace2");
-        assertThat(bundle.getStringArrayList(SearchSpec.SCHEMA_FIELD)).containsExactly(
+        assertThat(searchSpec.getFilterSchemas()).containsExactly(
                 "schemaTypes1", "schemaTypes2");
-        assertThat(bundle.getStringArrayList(SearchSpec.PACKAGE_NAME_FIELD)).containsExactly(
+        assertThat(searchSpec.getFilterPackageNames()).containsExactly(
                 "package1", "package2");
-        assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_FIELD)).isEqualTo(5);
-        assertThat(bundle.getInt(SearchSpec.SNIPPET_COUNT_PER_PROPERTY_FIELD)).isEqualTo(10);
-        assertThat(bundle.getInt(SearchSpec.MAX_SNIPPET_FIELD)).isEqualTo(15);
-        assertThat(bundle.getInt(SearchSpec.NUM_PER_PAGE_FIELD)).isEqualTo(42);
-        assertThat(bundle.getInt(SearchSpec.ORDER_FIELD)).isEqualTo(SearchSpec.ORDER_ASCENDING);
-        assertThat(bundle.getInt(SearchSpec.RANKING_STRATEGY_FIELD))
+        assertThat(searchSpec.getSnippetCount()).isEqualTo(5);
+        assertThat(searchSpec.getSnippetCountPerProperty()).isEqualTo(10);
+        assertThat(searchSpec.getMaxSnippetSize()).isEqualTo(15);
+        assertThat(searchSpec.getResultCountPerPage()).isEqualTo(42);
+        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+        assertThat(searchSpec.getRankingStrategy())
                 .isEqualTo(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE);
-        assertThat(bundle.getStringArrayList(SearchSpec.ENABLED_FEATURES_FIELD)).containsExactly(
+        assertThat(searchSpec.getEnabledFeatures()).containsExactly(
                 Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
                 Features.LIST_FILTER_QUERY_LANGUAGE);
     }
 
     @Test
+    public void testSearchSpecBuilderCopyConstructor() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterNamespaces("namespace1", "namespace2")
+                .addFilterSchemas("schemaTypes1", "schemaTypes2")
+                .addFilterPackageNames("package1", "package2")
+                .addFilterProperties("schemaTypes1", Arrays.asList("path1", "path2"))
+                .addProjection("schemaTypes1", Arrays.asList("path1", "path2"))
+                .setPropertyWeights("schemaTypes1", ImmutableMap.of("path1", 1.0, "path2", 2.0))
+                .setSnippetCount(5)
+                .setSnippetCountPerProperty(10)
+                .setMaxSnippetSize(15)
+                .setResultCountPerPage(42)
+                .setOrder(SearchSpec.ORDER_ASCENDING)
+                .setRankingStrategy("advancedExpression")
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, 10)
+                .setSearchSourceLogTag("searchSourceLogTag")
+                .build();
+
+        SearchSpec searchSpecCopy = new SearchSpec.Builder(searchSpec).build();
+        assertThat(searchSpecCopy.getTermMatch()).isEqualTo(searchSpec.getTermMatch());
+        assertThat(searchSpecCopy.getFilterNamespaces()).isEqualTo(
+                searchSpec.getFilterNamespaces());
+        assertThat(searchSpecCopy.getFilterSchemas()).isEqualTo(searchSpecCopy.getFilterSchemas());
+        assertThat(searchSpecCopy.getFilterPackageNames()).isEqualTo(
+                searchSpec.getFilterPackageNames());
+        assertThat(searchSpecCopy.getFilterProperties()).isEqualTo(
+                searchSpec.getFilterProperties());
+        assertThat(searchSpecCopy.getProjections()).isEqualTo(searchSpec.getProjections());
+        assertThat(searchSpecCopy.getPropertyWeights()).isEqualTo(searchSpec.getPropertyWeights());
+        assertThat(searchSpecCopy.getSnippetCount()).isEqualTo(searchSpec.getSnippetCount());
+        assertThat(searchSpecCopy.getSnippetCountPerProperty()).isEqualTo(
+                searchSpec.getSnippetCountPerProperty());
+        assertThat(searchSpecCopy.getMaxSnippetSize()).isEqualTo(searchSpec.getMaxSnippetSize());
+        assertThat(searchSpecCopy.getResultCountPerPage()).isEqualTo(
+                searchSpec.getResultCountPerPage());
+        assertThat(searchSpecCopy.getOrder()).isEqualTo(searchSpec.getOrder());
+        assertThat(searchSpecCopy.getRankingStrategy()).isEqualTo(searchSpec.getRankingStrategy());
+        assertThat(searchSpecCopy.getEnabledFeatures()).containsExactlyElementsIn(
+                searchSpec.getEnabledFeatures());
+        assertThat(searchSpecCopy.getResultGroupingTypeFlags()).isEqualTo(
+                searchSpec.getResultGroupingTypeFlags());
+        assertThat(searchSpecCopy.getResultGroupingLimit()).isEqualTo(
+                searchSpec.getResultGroupingLimit());
+        assertThat(searchSpecCopy.getJoinSpec()).isEqualTo(searchSpec.getJoinSpec());
+        assertThat(searchSpecCopy.getAdvancedRankingExpression()).isEqualTo(
+                searchSpec.getAdvancedRankingExpression());
+        assertThat(searchSpecCopy.getSearchSourceLogTag()).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() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+
+        assertThat(searchSpec.getEnabledFeatures()).containsExactly(
+                Features.NUMERIC_SEARCH, Features.VERBATIM_SEARCH,
+                Features.LIST_FILTER_QUERY_LANGUAGE, Features.LIST_FILTER_HAS_PROPERTY_FUNCTION);
+    }
+
+    @Test
     public void testBuildMultipleSearchSpecs() {
         SearchSpec.Builder builder = new SearchSpec.Builder();
         SearchSpec searchSpec1 = builder.build();
@@ -90,37 +192,46 @@
                 Features.VERBATIM_SEARCH, Features.LIST_FILTER_QUERY_LANGUAGE);
     }
 
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
     @Test
-    public void testGetPropertyFiltersTypePropertyMasks() {
+    public void testGetEnabledFeatures_embeddingSearch() {
         SearchSpec searchSpec = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
-                .addFilterProperties("TypeB", ImmutableList.of("field7"))
-                .addFilterProperties("TypeC", ImmutableList.of())
+                .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);
 
-        Map> typePropertyPathMap = searchSpec.getFilterProperties();
-        assertThat(typePropertyPathMap.keySet())
-                .containsExactly("TypeA", "TypeB", "TypeC");
-        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
-        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
-        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+        // 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);
     }
 
-    // TODO(b/296088047): move to CTS once the APIs it uses are public
     @Test
-    public void testBuilder_throwsException_whenTypePropertyFilterNotInSchemaFilter() {
-        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addFilterSchemas("Schema1", "Schema2")
-                .addFilterPropertyPaths("Schema3", ImmutableList.of(
-                        new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
+    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);
 
-        IllegalStateException exception =
-                assertThrows(IllegalStateException.class, searchSpecBuilder::build);
-        assertThat(exception.getMessage())
-                .isEqualTo("The schema: Schema3 exists in the property filter but doesn't"
-                        + " exist in the schema filter.");
+        // 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/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
deleted file mode 100644
index bc68f37..0000000
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSuggestionSpecInternalTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 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 androidx.appsearch.app;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import com.google.common.collect.ImmutableList;
-
-import org.junit.Test;
-
-// TODO(b/228240987) delete this test when we support property restrict for multiple terms
-public class SearchSuggestionSpecInternalTest {
-
-    @Test
-    public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {
-        SearchSuggestionSpec searchSuggestionSpec =
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
-                        .setRankingStrategy(SearchSuggestionSpec
-                                .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
-                        .addFilterSchemas("Person", "Email")
-                        .addFilterSchemas(ImmutableList.of("Foo"))
-                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
-                        .addFilterPropertyPaths("Foo",
-                                ImmutableList.of(new PropertyPath("Bar")))
-                        .build();
-
-        assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);
-        assertThat(searchSuggestionSpec.getFilterSchemas())
-                .containsExactly("Person", "Email", "Foo");
-        assertThat(searchSuggestionSpec.getFilterProperties())
-                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
-                        "Foo",  ImmutableList.of("Bar"));
-    }
-
-    @Test
-    public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
-        IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
-                        .addFilterSchemas("Person")
-                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
-                        .build());
-        assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
-                + "property filter but doesn't exist in the schema filter.");
-    }
-
-    @Test
-    public void testRebuild_withPropertyFilter() throws Exception {
-        SearchSuggestionSpec.Builder builder =
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
-                        .addFilterSchemas("Person", "Email")
-                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"));
-
-        SearchSuggestionSpec original = builder.build();
-
-        builder.addFilterSchemas("Message", "Foo")
-                .addFilterProperties("Foo", ImmutableList.of("Bar"));
-        SearchSuggestionSpec rebuild = builder.build();
-
-        assertThat(original.getMaximumResultCount()).isEqualTo(123);
-        assertThat(original.getFilterSchemas())
-                .containsExactly("Person", "Email");
-        assertThat(original.getFilterProperties())
-                .containsExactly("Email",  ImmutableList.of("Subject", "body"));
-
-        assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);
-        assertThat(rebuild.getFilterSchemas())
-                .containsExactly("Person", "Email", "Message", "Foo");
-        assertThat(rebuild.getFilterProperties())
-                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
-                        "Foo",  ImmutableList.of("Bar"));
-    }
-}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
index 37e1255..145679a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaResponseInternalTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 
@@ -55,7 +53,7 @@
         assertThat(original.getMigratedTypes()).containsExactly("migrated1");
         assertThat(original.getMigrationFailures()).containsExactly(failure1);
 
-        SetSchemaResponse rebuild = original.toBuilder()
+        SetSchemaResponse rebuild = new SetSchemaResponse.Builder(original)
                         .addDeletedType("delete2")
                         .addIncompatibleType("incompatible2")
                         .addMigratedType("migrated2")
@@ -82,7 +80,6 @@
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("qualifiedId1")
                         .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
                         .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .setDeletionPropagation(true)
                         .build())
                 .build();
 
@@ -95,20 +92,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/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 4118c45..0e4952c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -25,13 +25,23 @@
 import androidx.appsearch.app.AppSearchSchema.LongPropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.PropertyPath;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 import androidx.appsearch.testutil.AppSearchEmail;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Collections;
+import java.util.List;
 
 public class AppSearchSchemaCtsTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void testInvalidEnums() {
         StringPropertyConfig.Builder builder = new StringPropertyConfig.Builder("test");
@@ -165,6 +175,243 @@
     }
 
     @Test
+    public void testParentTypes() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("EmailMessage")
+                        .addParentType("Email")
+                        .addParentType("Message")
+                        .build();
+        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+    }
+
+    @Test
+    public void testDuplicateParentTypes() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("EmailMessage")
+                        .addParentType("Email")
+                        .addParentType("Message")
+                        .addParentType("Email")
+                        .build();
+        assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+    }
+
+    @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");
+    }
+
+    @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");
+    }
+
+    @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");
+    }
+
+    @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");
+    }
+
+    @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 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);
+    }
+
+    @Test
     public void testInvalidStringPropertyConfigsTokenizerNone() {
         // Everything should work fine with the defaults.
         final StringPropertyConfig.Builder builder =
@@ -195,6 +442,57 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testEquals_failure_differentDescription() {
+        AppSearchSchema.Builder schemaBuilder =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build());
+        AppSearchSchema schema1 = schemaBuilder.build();
+        AppSearchSchema schema2 =
+                schemaBuilder.setDescription("Mail, but like with an 'e'").build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testEquals_failure_differentPropertyDescription() {
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the contents of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                .build();
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("The beginning of a message.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                .build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
     public void testInvalidStringPropertyConfigsTokenizerNonNone() {
         // Setting indexing type to be NONE with tokenizer type PLAIN or VERBATIM or RFC822 should
         // fail. Regardless of whether NONE is set explicitly or just kept as default.
@@ -250,162 +548,253 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
     public void testAppSearchSchema_toString() {
-        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
-                .addProperty(new StringPropertyConfig.Builder("string1")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string2")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string3")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string4")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("string5")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
-                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("qualifiedId1")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .build())
-                .addProperty(new StringPropertyConfig.Builder("qualifiedId2")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                        .build())
-                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("long")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
-                        .build())
-                .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
-                        .build())
-                .addProperty(new AppSearchSchema.DoublePropertyConfig.Builder("double")
-                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
-                .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
-                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
-                        .build())
-                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
-                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
-                        .build())
-                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                        "document", AppSearchEmail.SCHEMA_TYPE)
-                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
-                        .setShouldIndexNestedProperties(true)
-                        .build())
-                .build();
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .setDescription("a test schema")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string1")
+                                        .setDescription("first string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string2")
+                                        .setDescription("second string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string3")
+                                        .setDescription("third string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string4")
+                                        .setDescription("fourth string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string5")
+                                        .setDescription("fifth string")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("qualifiedId1")
+                                        .setDescription("first qualifiedId")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setJoinableValueType(
+                                                StringPropertyConfig
+                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("qualifiedId2")
+                                        .setDescription("second qualifiedId")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setJoinableValueType(
+                                                StringPropertyConfig
+                                                        .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("long")
+                                        .setDescription("a long")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_NONE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.LongPropertyConfig.Builder("indexableLong")
+                                        .setDescription("an indexed long")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(LongPropertyConfig.INDEXING_TYPE_RANGE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DoublePropertyConfig.Builder("double")
+                                        .setDescription("a double")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BooleanPropertyConfig.Builder("boolean")
+                                        .setDescription("a boolean")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                                        .setDescription("some bytes")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                                "document", AppSearchEmail.SCHEMA_TYPE)
+                                        .setDescription("a document")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .build())
+                        .build();
 
         String schemaString = schema.toString();
 
-        String expectedString = "{\n"
-                + "  schemaType: \"testSchema\",\n"
-                + "  properties: [\n"
-                + "    {\n"
-                + "      name: \"boolean\",\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_BOOLEAN,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"bytes\",\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_BYTES,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"document\",\n"
-                + "      shouldIndexNestedProperties: true,\n"
-                + "      schemaType: \"builtin:Email\",\n"
-                + "      cardinality: CARDINALITY_REPEATED,\n"
-                + "      dataType: DATA_TYPE_DOCUMENT,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"double\",\n"
-                + "      cardinality: CARDINALITY_REPEATED,\n"
-                + "      dataType: DATA_TYPE_DOUBLE,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"indexableLong\",\n"
-                + "      indexingType: INDEXING_TYPE_RANGE,\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_LONG,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"long\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_LONG,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"qualifiedId1\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"qualifiedId2\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
-                + "      cardinality: CARDINALITY_OPTIONAL,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string1\",\n"
-                + "      indexingType: INDEXING_TYPE_NONE,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string2\",\n"
-                + "      indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string3\",\n"
-                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string4\",\n"
-                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    },\n"
-                + "    {\n"
-                + "      name: \"string5\",\n"
-                + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
-                + "      tokenizerType: TOKENIZER_TYPE_RFC822,\n"
-                + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
-                + "      cardinality: CARDINALITY_REQUIRED,\n"
-                + "      dataType: DATA_TYPE_STRING,\n"
-                + "    }\n"
-                + "  ]\n"
-                + "}";
+        String expectedString =
+                "{\n"
+                        + "  schemaType: \"testSchema\",\n"
+                        + "  description: \"a test schema\",\n"
+                        + "  properties: [\n"
+                        + "    {\n"
+                        + "      name: \"boolean\",\n"
+                        + "      description: \"a boolean\",\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_BOOLEAN,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"bytes\",\n"
+                        + "      description: \"some bytes\",\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_BYTES,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"document\",\n"
+                        + "      description: \"a document\",\n"
+                        + "      shouldIndexNestedProperties: true,\n"
+                        + "      schemaType: \"builtin:Email\",\n"
+                        + "      cardinality: CARDINALITY_REPEATED,\n"
+                        + "      dataType: DATA_TYPE_DOCUMENT,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"double\",\n"
+                        + "      description: \"a double\",\n"
+                        + "      cardinality: CARDINALITY_REPEATED,\n"
+                        + "      dataType: DATA_TYPE_DOUBLE,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"indexableLong\",\n"
+                        + "      description: \"an indexed long\",\n"
+                        + "      indexingType: INDEXING_TYPE_RANGE,\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_LONG,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"long\",\n"
+                        + "      description: \"a long\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_LONG,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"qualifiedId1\",\n"
+                        + "      description: \"first qualifiedId\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"qualifiedId2\",\n"
+                        + "      description: \"second qualifiedId\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"
+                        + "      cardinality: CARDINALITY_OPTIONAL,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string1\",\n"
+                        + "      description: \"first string\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string2\",\n"
+                        + "      description: \"second string\",\n"
+                        + "      indexingType: INDEXING_TYPE_EXACT_TERMS,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string3\",\n"
+                        + "      description: \"third string\",\n"
+                        + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_PLAIN,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string4\",\n"
+                        + "      description: \"fourth string\",\n"
+                        + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    },\n"
+                        + "    {\n"
+                        + "      name: \"string5\",\n"
+                        + "      description: \"fifth string\",\n"
+                        + "      indexingType: INDEXING_TYPE_PREFIXES,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_RFC822,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    }\n"
+                        + "  ]\n"
+                        + "}";
+
+        String[] lines = expectedString.split("\n");
+        for (String line : lines) {
+            assertThat(schemaString).contains(line);
+        }
+    }
+
+    @Test
+    public void testAppSearchSchema_toStringNoDescriptionSet() {
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("testSchema")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("string1")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_NONE)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_NONE)
+                                        .build())
+                        .build();
+
+        String schemaString = schema.toString();
+
+        String expectedString =
+                          "{\n"
+                        + "  schemaType: \"testSchema\",\n"
+                        + "  description: \"\",\n"
+                        + "  properties: [\n"
+                        + "    {\n"
+                        + "      name: \"string1\",\n"
+                        + "      description: \"\",\n"
+                        + "      indexingType: INDEXING_TYPE_NONE,\n"
+                        + "      tokenizerType: TOKENIZER_TYPE_NONE,\n"
+                        + "      joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"
+                        + "      cardinality: CARDINALITY_REQUIRED,\n"
+                        + "      dataType: DATA_TYPE_STRING,\n"
+                        + "    }\n"
+                        + "  ]\n"
+                        + "}";
 
         String[] lines = expectedString.split("\n");
         for (String line : lines) {
@@ -466,4 +855,120 @@
                         "DocumentIndexingConfig#shouldIndexNestedProperties is required to be false"
                                 + " when one or more indexableNestedProperties are provided.");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingPropertyConfig() {
+        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("indexableLong")
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                AppSearchSchema.LongPropertyConfig
+                                                        .INDEXING_TYPE_RANGE)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "document1", AppSearchEmail.SCHEMA_TYPE)
+                                        .setCardinality(
+                                                AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                                        .setShouldIndexNestedProperties(true)
+                                        .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();
+
+        assertThat(schema.getSchemaType()).isEqualTo("Test");
+        List properties = schema.getProperties();
+        assertThat(properties).hasSize(5);
+
+        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("indexableLong");
+        assertThat(properties.get(1).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.LongPropertyConfig) properties.get(1)).getIndexingType())
+                .isEqualTo(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE);
+
+        assertThat(properties.get(2).getName()).isEqualTo("document1");
+        assertThat(properties.get(2).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED);
+        assertThat(((AppSearchSchema.DocumentPropertyConfig) properties.get(2)).getSchemaType())
+                .isEqualTo(AppSearchEmail.SCHEMA_TYPE);
+        assertThat(
+                ((AppSearchSchema.DocumentPropertyConfig) properties.get(2))
+                        .shouldIndexNestedProperties())
+                .isEqualTo(true);
+
+        assertThat(properties.get(3).getName()).isEqualTo("embedding");
+        assertThat(properties.get(3).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.EmbeddingPropertyConfig) properties.get(3)).getIndexingType())
+                .isEqualTo(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE);
+
+        assertThat(properties.get(4).getName()).isEqualTo("indexableEmbedding");
+        assertThat(properties.get(4).getCardinality())
+                .isEqualTo(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+        assertThat(((AppSearchSchema.EmbeddingPropertyConfig) properties.get(4)).getIndexingType())
+                .isEqualTo(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingPropertyConfig_defaultValues() {
+        AppSearchSchema.EmbeddingPropertyConfig builder =
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("test").build();
+        assertThat(builder.getIndexingType()).isEqualTo(
+                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE);
+        assertThat(builder.getCardinality()).isEqualTo(
+                AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingPropertyConfig_setIndexingType() {
+        assertThrows(IllegalArgumentException.class, () ->
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+                        .setIndexingType(5).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+                        .setIndexingType(2).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new AppSearchSchema.EmbeddingPropertyConfig.Builder("titleEmbedding")
+                        .setIndexingType(-1).build());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
index 59c17f5..895a58a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationCtsTestBase.java
@@ -154,9 +154,11 @@
     }
 
     @Test
-    public void testSchemaMigration_A_B_C_D() throws Exception {
+    public void test_ForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema and update the version
-        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -166,7 +168,7 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .setVersion(2)     // upgrade version
@@ -174,9 +176,11 @@
     }
 
     @Test
-    public void testSchemaMigration_A_B_NC_D() throws Exception {
+    public void testForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema but don't update the version
-        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleNoTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -186,20 +190,22 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleNoTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_A_NB_C_D() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .setVersion(2)     // upgrade version
@@ -207,13 +213,15 @@
     }
 
     @Test
-    public void testSchemaMigration_A_NB_C_ND() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
                         .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                         .setForceOverride(true)
                         .setVersion(2)     // upgrade version
@@ -221,35 +229,42 @@
     }
 
     @Test
-    public void testSchemaMigration_A_NB_NC_D() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_NoTrigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema but don't update the version
-        AppSearchSchema NB_NC_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleNoTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_NC_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleNoTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_A_NB_NC_ND() throws Exception {
+    public void testForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema but don't update the version
-        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleNoMigrateIncompatibleTypeSchema =
+                new AppSearchSchema.Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(
+                                backwardsIncompatibleNoMigrateIncompatibleTypeSchema)
                         .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_B_C_D() throws Exception {
+    public void testNoForceOverride_BackwardsCompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema and update the version
-        AppSearchSchema B_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -259,16 +274,18 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setVersion(2)     // upgrade version
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_B_NC_D() throws Exception {
+    public void testNoForceOverride_BackwardsCompatible_NoTrigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards compatible schema but don't update the version
-        AppSearchSchema B_NC_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleNoTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
                         .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                         .setIndexingType(
@@ -278,34 +295,38 @@
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(B_NC_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleNoTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setForceOverride(true)
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_NB_C_D() throws Exception {
+    public void testNoForceOverride_BackwardsIncompatible_Trigger_MigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema NB_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         mDb.setSchemaAsync(
-                new SetSchemaRequest.Builder().addSchemas(NB_C_Schema)
+                new SetSchemaRequest.Builder().addSchemas(backwardsIncompatibleTriggerSchema)
                         .setMigrator("testSchema", ACTIVE_NOOP_MIGRATOR)
                         .setVersion(2)     // upgrade version
                         .build()).get();
     }
 
     @Test
-    public void testSchemaMigration_NA_NB_C_ND() throws Exception {
+    public void testNoForceOverride_BackwardsIncompatible_Trigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema and update the version
-        AppSearchSchema $B_C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsCompatibleTriggerSchema = new AppSearchSchema
+                .Builder("testSchema")
                 .build();
 
         ExecutionException exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas($B_C_Schema)
+                        new SetSchemaRequest.Builder().addSchemas(backwardsCompatibleTriggerSchema)
                                 .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                                 .setVersion(2)     // upgrade version
                                 .build()).get());
@@ -313,14 +334,17 @@
     }
 
     @Test
-    public void testSchemaMigration_NA_NB_NC_ND() throws Exception {
+    public void testNoForceOverride_BackwardsIncompatible_NoTrigger_NoMigrateIncompatibleType()
+            throws Exception {
         // create a backwards incompatible schema but don't update the version
-        AppSearchSchema $B_$C_Schema = new AppSearchSchema.Builder("testSchema")
+        AppSearchSchema backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema =
+                new AppSearchSchema.Builder("testSchema")
                 .build();
 
         ExecutionException exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(
-                        new SetSchemaRequest.Builder().addSchemas($B_$C_Schema)
+                        new SetSchemaRequest.Builder().addSchemas(
+                                backwardsIncompatibleNoTriggerNoMigrateIncompatibleTypeSchema)
                                 .setMigrator("testSchema", INACTIVE_MIGRATOR)  //ND
                                 .build()).get());
         assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
@@ -704,7 +728,7 @@
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
 
-        Migrator migrator_sourceToNowhere = new Migrator() {
+        Migrator migratorSourceToNowhere = new Migrator() {
             @Override
             public boolean shouldMigrate(int currentVersion, int finalVersion) {
                 return true;
@@ -732,7 +756,7 @@
         ExecutionException exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setMigrator("sourceSchema", migratorSourceToNowhere)
                         .setVersion(2).build())   // upgrade version
                         .get());
         assertThat(exception).hasMessageThat().contains(
@@ -744,7 +768,7 @@
         exception = assertThrows(ExecutionException.class,
                 () -> mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("sourceSchema", migrator_sourceToNowhere)
+                        .setMigrator("sourceSchema", migratorSourceToNowhere)
                         .setForceOverride(true)
                         .setVersion(2).build())   // upgrade version
                         .get());
@@ -761,7 +785,7 @@
         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .addSchemas(destinationSchema).setForceOverride(true).build()).get();
 
-        Migrator migrator_nowhereToDestination = new Migrator() {
+        Migrator migratorNowhereToDestination = new Migrator() {
             @Override
             public boolean shouldMigrate(int currentVersion, int finalVersion) {
                 return true;
@@ -789,7 +813,7 @@
         SetSchemaResponse setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setMigrator("nonExistSchema", migratorNowhereToDestination)
                         .setVersion(2) //  upgrade version
                         .build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -798,7 +822,7 @@
         setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(destinationSchema)
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToDestination)
+                        .setMigrator("nonExistSchema", migratorNowhereToDestination)
                         .setVersion(2) //  upgrade version
                         .setForceOverride(true).build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -809,7 +833,7 @@
         // set empty schema
         mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                 .setForceOverride(true).build()).get();
-        Migrator migrator_nowhereToNowhere = new Migrator() {
+        Migrator migratorNowhereToNowhere = new Migrator() {
             @Override
             public boolean shouldMigrate(int currentVersion, int finalVersion) {
                 return true;
@@ -837,7 +861,7 @@
         SetSchemaResponse setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setMigrator("nonExistSchema", migratorNowhereToNowhere)
                         .setVersion(2)  //  upgrade version
                         .build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -846,7 +870,7 @@
         setSchemaResponse =
                 mDb.setSchemaAsync(new SetSchemaRequest.Builder()
                         .addSchemas(new AppSearchSchema.Builder("emptySchema").build())
-                        .setMigrator("nonExistSchema", migrator_nowhereToNowhere)
+                        .setMigrator("nonExistSchema", migratorNowhereToNowhere)
                         .setVersion(2) //  upgrade version
                         .setForceOverride(true).build()).get();
         assertThat(setSchemaResponse.getMigratedTypes()).isEmpty();
@@ -1208,7 +1232,7 @@
         @Override
         public GenericDocument onUpgrade(int currentVersion, int finalVersion,
                 @NonNull GenericDocument document) {
-            GenericDocument.Builder docBuilder =
+            GenericDocument.Builder docBuilder =
                     new GenericDocument.Builder<>("namespace", "id", "TypeB")
                             .setCreationTimestampMillis(DOCUMENT_CREATION_TIME);
             if (currentVersion == 2) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index 672c0f3..97698a7 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -41,6 +41,7 @@
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
@@ -51,6 +52,7 @@
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.RemoveByDocumentIdRequest;
 import androidx.appsearch.app.ReportUsageRequest;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
@@ -60,7 +62,13 @@
 import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.cts.app.customer.EmailDocument;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
 import androidx.appsearch.util.DocumentIdUtil;
 import androidx.collection.ArrayMap;
 import androidx.test.core.app.ApplicationProvider;
@@ -73,6 +81,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -89,6 +98,14 @@
     static final String DB_NAME_1 = "";
     static final String DB_NAME_2 = "testDb2";
 
+    // Since we cannot call non-public API in the cts test, make a copy of these 2 action types, so
+    // we can create taken actions in GenericDocument form.
+    private static final int ACTION_TYPE_SEARCH = 1;
+    private static final int ACTION_TYPE_CLICK = 2;
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private final Context mContext = ApplicationProvider.getApplicationContext();
 
     private AppSearchSession mDb1;
@@ -170,6 +187,178 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_schemaDescription_notSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SCHEMA_SET_DESCRIPTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
+                .setDescription("Unsupported description")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_propertyDescription_notSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SCHEMA_SET_DESCRIPTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email1")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setDescription("Unsupported description")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(exception).hasMessageThat().contains(Features.SCHEMA_SET_DESCRIPTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_updateSchemaDescription() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
+
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All of the content of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
+                .get();
+
+        Set actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema1);
+
+        // Change the type description.
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("Like mail but with an 'a'.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All of the content of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
+                .get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTIONS)  // setDescription
+    public void testSetSchema_updatePropertyDescription() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_SET_DESCRIPTION));
+
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("A summary of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All of the content of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema1).build())
+                .get();
+
+        Set actualSchemaTypes = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(actualSchemaTypes).containsExactly(schema1);
+
+        // Change the type description.
+        AppSearchSchema schema2 =
+                new AppSearchSchema.Builder("Email")
+                        .setDescription("A type of electronic message.")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("subject")
+                                        .setDescription("The most important part of the email.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("body")
+                                        .setDescription("All the other stuff.")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema2).build())
+                .get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(schema2);
+    }
+
+    @Test
     public void testSetSchema_updateVersion() throws Exception {
         AppSearchSchema schema = new AppSearchSchema.Builder("Email")
                 .addProperty(new StringPropertyConfig.Builder("subject")
@@ -420,6 +609,46 @@
     }
 
     @Test
+    public void testSetSchemaWithInvalidCycle_circularReferencesSupported() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
+
+        // Create schema with invalid cycle: Person -> Organization -> Person... where all
+        // DocumentPropertyConfigs have setShouldIndexNestedProperties(true).
+        AppSearchSchema personSchema = new AppSearchSchema.Builder("Person")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new DocumentPropertyConfig.Builder("worksFor", "Organization")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+        AppSearchSchema organizationSchema = new AppSearchSchema.Builder("Organization")
+                .addProperty(new StringPropertyConfig.Builder("name")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build())
+                .addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder().addSchemas(personSchema, organizationSchema).build();
+        ExecutionException executionException =
+                assertThrows(ExecutionException.class,
+                        () -> mDb1.setSchemaAsync(setSchemaRequest).get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().containsMatch("Invalid cycle|Infinite loop");
+    }
+
+    @Test
     public void testSetSchemaWithValidCycle_circularReferencesNotSupported() {
         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
 
@@ -467,8 +696,6 @@
     }
 // @exportToFramework:endStrip()
 
-// @exportToFramework:startStrip()
-
     /** Test indexing maximum properties into a schema. */
     @Test
     public void testSetSchema_maxProperties() throws Exception {
@@ -485,11 +712,89 @@
         Set actual1 = mDb1.getSchemaAsync().get().getSchemas();
         assertThat(actual1).containsExactly(maxSchema);
 
-        // TODO(b/300135897): Expand test to assert adding more than allowed properties is
-        //  fixed once fixed.
+        schemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                .build());
+        ExecutionException exception = assertThrows(ExecutionException.class, () ->
+                mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                        .addSchemas(schemaBuilder.build()).setForceOverride(true).build()).get());
+        Throwable cause = exception.getCause();
+        assertThat(cause).isInstanceOf(AppSearchException.class);
+        assertThat(cause.getMessage()).isEqualTo("Too many properties to be indexed, max "
+                + "number of properties allowed: " + maxProperties);
     }
 
     @Test
+    public void testSetSchema_maxProperties_nestedSchemas() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SET_SCHEMA_CIRCULAR_REFERENCES));
+
+        int maxProperties = mDb1.getFeatures().getMaxIndexedProperties();
+        AppSearchSchema.Builder personSchemaBuilder = new AppSearchSchema.Builder("Person");
+        for (int i = 0; i < maxProperties / 3 + 1; i++) {
+            personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+        personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("worksFor",
+                "Organization")
+                .setShouldIndexNestedProperties(false)
+                .build());
+        personSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("address", "Address")
+                .setShouldIndexNestedProperties(true)
+                .build());
+
+        AppSearchSchema.Builder orgSchemaBuilder = new AppSearchSchema.Builder("Organization");
+        for (int i = 0; i < maxProperties / 3; i++) {
+            orgSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+        orgSchemaBuilder.addProperty(new DocumentPropertyConfig.Builder("funder", "Person")
+                .setShouldIndexNestedProperties(true)
+                .build());
+
+        AppSearchSchema.Builder addressSchemaBuilder = new AppSearchSchema.Builder("Address");
+        for (int i = 0; i < maxProperties / 3; i++) {
+            addressSchemaBuilder.addProperty(new StringPropertyConfig.Builder("string" + i)
+                    .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                    .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                    .build());
+        }
+
+        AppSearchSchema personSchema = personSchemaBuilder.build();
+        AppSearchSchema orgSchema = orgSchemaBuilder.build();
+        AppSearchSchema addressSchema = addressSchemaBuilder.build();
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(personSchema, orgSchema, addressSchema)
+                        .build()).get();
+        Set schemas = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(schemas).containsExactly(personSchema, orgSchema, addressSchema);
+
+        // Add one more property to bring the number of sections over the max limit
+        personSchemaBuilder.addProperty(new StringPropertyConfig.Builder("toomuch")
+                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                .build());
+        ExecutionException exception = assertThrows(ExecutionException.class,
+                () -> mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(personSchemaBuilder.build(), orgSchema, addressSchema)
+                                .setForceOverride(true)
+                                .build()
+                ).get());
+        Throwable cause = exception.getCause();
+        assertThat(cause).isInstanceOf(AppSearchException.class);
+        assertThat(cause.getMessage()).contains("Too many properties to be indexed");
+    }
+
+// @exportToFramework:startStrip()
+
+    @Test
     public void testGetSchema() throws Exception {
         AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
                 .addProperty(new StringPropertyConfig.Builder("subject")
@@ -672,6 +977,58 @@
     }
 
     @Test
+    public void testGetSchema_visibilitySetting_oneSharedSchema() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
+
+        AppSearchSchema noteSchema = new AppSearchSchema.Builder("Note")
+                .addProperty(new StringPropertyConfig.Builder("subject").build()).build();
+        SetSchemaRequest.Builder requestBuilder = new SetSchemaRequest.Builder()
+                .addSchemas(AppSearchEmail.SCHEMA, noteSchema)
+                .setSchemaTypeDisplayedBySystem(noteSchema.getSchemaType(), false)
+                .setSchemaTypeVisibilityForPackage(
+                        noteSchema.getSchemaType(),
+                        true,
+                        new PackageIdentifier("com.some.package1", new byte[32]))
+                .addRequiredPermissionsForSchemaTypeVisibility(
+                        noteSchema.getSchemaType(),
+                        Collections.singleton(SetSchemaRequest.READ_SMS));
+        if (mDb1.getFeatures().isFeatureSupported(
+                Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
+            requestBuilder.setPubliclyVisibleSchema(
+                    noteSchema.getSchemaType(),
+                    new PackageIdentifier("com.some.package2", new byte[32]));
+        }
+        SetSchemaRequest request = requestBuilder.build();
+        mDb1.setSchemaAsync(request).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        Set actual = getSchemaResponse.getSchemas();
+        assertThat(actual).hasSize(2);
+        assertThat(actual).isEqualTo(request.getSchemas());
+
+        // Check visibility settings. Schemas without settings shouldn't appear in the result at
+        // all, even with empty maps as values.
+        assertThat(getSchemaResponse.getSchemaTypesNotDisplayedBySystem())
+                .containsExactly(noteSchema.getSchemaType());
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToPackages())
+                .containsExactly(
+                        noteSchema.getSchemaType(),
+                        ImmutableSet.of(new PackageIdentifier("com.some.package1", new byte[32])));
+        assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility())
+                .containsExactly(
+                        noteSchema.getSchemaType(),
+                        ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_SMS)));
+        if (mDb1.getFeatures().isFeatureSupported(
+                Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE)) {
+            assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
+                    .containsExactly(
+                            noteSchema.getSchemaType(),
+                            new PackageIdentifier("com.some.package2", new byte[32]));
+        }
+    }
+
+    @Test
     public void testGetSchema_visibilitySetting_notSupported() throws Exception {
         assumeFalse(mDb1.getFeatures().isFeatureSupported(
                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY));
@@ -740,6 +1097,101 @@
     }
 
     @Test
+    public void testSetSchema_publiclyVisible() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
+
+        PackageIdentifier pkg = new PackageIdentifier(mContext.getPackageName(), new byte[32]);
+        SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
+                .setPubliclyVisibleSchema("builtin:Email", pkg).build();
+
+        mDb1.setSchemaAsync(request).get();
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+        assertThat(getSchemaResponse.getPubliclyVisibleSchemas())
+                .isEqualTo(ImmutableMap.of("builtin:Email", pkg));
+
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace", "id1")
+                .setSubject("testPut example").build();
+
+        // mDb1 and mDb2 are in the same package, so we can't REALLY test out public acl. But we
+        // can make sure they their own documents under the Public ACL.
+        AppSearchBatchResult putResult =
+                checkIsBatchResultSuccess(mDb1.putAsync(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(email).build()));
+        assertThat(putResult.getSuccesses()).containsExactly("id1", null);
+        assertThat(putResult.getFailures()).isEmpty();
+
+        GetByDocumentIdRequest getByDocumentIdRequest =
+                new GetByDocumentIdRequest.Builder("namespace")
+                        .addIds("id1")
+                        .build();
+        List outDocuments = doGet(mDb1, getByDocumentIdRequest);
+        assertThat(outDocuments).hasSize(1);
+        assertThat(outDocuments).containsExactly(email);
+    }
+
+    @Test
+    public void testSetSchema_publiclyVisible_unsupported() {
+        assumeFalse(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE));
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("Email").build())
+                .setPubliclyVisibleSchema("Email",
+                        new PackageIdentifier(mContext.getPackageName(), new byte[32])).build();
+        Exception e = assertThrows(UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(e.getMessage()).isEqualTo("Publicly visible schema are not supported on this "
+                + "AppSearch implementation.");
+    }
+
+    @Test
+    public void testSetSchema_visibleToConfig() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
+        byte[] cert1 = new byte[32];
+        byte[] cert2 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        Arrays.fill(cert2, (byte) 2);
+        PackageIdentifier pkg1 = new PackageIdentifier("package1", cert1);
+        PackageIdentifier pkg2 = new PackageIdentifier("package2", cert2);
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .setPubliclyVisibleTargetPackage(pkg1)
+                .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .setPubliclyVisibleTargetPackage(pkg2)
+                .addRequiredPermissions(ImmutableSet.of(3, 4)).build();
+        SetSchemaRequest request = new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA)
+                .addSchemaTypeVisibleToConfig("builtin:Email", config1)
+                .addSchemaTypeVisibleToConfig("builtin:Email", config2)
+                .build();
+        mDb1.setSchemaAsync(request).get();
+
+        GetSchemaResponse getSchemaResponse = mDb1.getSchemaAsync().get();
+        assertThat(getSchemaResponse.getSchemas()).containsExactly(AppSearchEmail.SCHEMA);
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs())
+                .isEqualTo(ImmutableMap.of("builtin:Email", ImmutableSet.of(config1, config2)));
+    }
+
+    @Test
+    public void testSetSchema_visibleToConfig_unsupported() {
+        assumeFalse(mDb1.getFeatures()
+                .isFeatureSupported(Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG));
+
+        SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
+                .addRequiredPermissions(ImmutableSet.of(1, 2)).build();
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(new AppSearchSchema.Builder("Email").build())
+                .addSchemaTypeVisibleToConfig("Email", config).build();
+        Exception e = assertThrows(UnsupportedOperationException.class,
+                () -> mDb1.setSchemaAsync(request).get());
+        assertThat(e.getMessage()).isEqualTo("Schema visible to config are not supported on"
+                + " this AppSearch implementation.");
+    }
+
+    @Test
     public void testGetSchema_longPropertyIndexingType() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.NUMERIC_SEARCH));
         AppSearchSchema inSchema = new AppSearchSchema.Builder("Test")
@@ -1112,9 +1564,106 @@
         assertThat(result.getSuccesses()).containsExactly("id1", null);
         assertThat(result.getFailures()).isEmpty();
     }
+
+    @Test
+    public void testPutDocuments_takenActions() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addDocumentClasses(SearchAction.class, ClickAction.class)
+                                .build())
+                .get();
+
+        // Put a SearchAction and ClickAction document
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "search", /* actionTimestampMillis= */1000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("query")
+                        .setFetchedResultCount(10)
+                        .build();
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "click", /* actionTimestampMillis= */2000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(1024)
+                        .build();
+
+        AppSearchBatchResult result = checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addTakenActions(searchAction, clickAction)
+                        .build()));
+        assertThat(result.getSuccesses()).containsEntry("search", null);
+        assertThat(result.getSuccesses()).containsEntry("click", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
 // @exportToFramework:endStrip()
 
     @Test
+    public void testPutDocuments_takenActionGenericDocuments() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // Schema registration
+        AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .build()
+                ).build();
+
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(searchActionSchema, clickActionSchema)
+                        .build()).get();
+
+        // Put search action and click action generic documents.
+        GenericDocument searchAction =
+                new GenericDocument.Builder<>("namespace", "search", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
+                        .setPropertyString("query", "body")
+                        .build();
+        GenericDocument clickAction =
+                new GenericDocument.Builder<>("namespace", "click", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", "pkg$db/ns#refId")
+                        .build();
+
+        AppSearchBatchResult result = checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addTakenActionGenericDocuments(searchAction, clickAction)
+                        .build()));
+        assertThat(result.getSuccesses()).containsEntry("search", null);
+        assertThat(result.getSuccesses()).containsEntry("click", null);
+        assertThat(result.getFailures()).isEmpty();
+    }
+
+    @Test
     public void testUpdateSchema() throws Exception {
         // Schema registration
         AppSearchSchema oldEmailSchema = new AppSearchSchema.Builder(AppSearchEmail.SCHEMA_TYPE)
@@ -2112,6 +2661,247 @@
         assertThat(sr.get(0).getRankingSignal()).isEqualTo(6.0);
     }
 
+// @exportToFramework:startStrip()
+    @Test
+    public void testQueryRankByClickActions_useTakenAction() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder()
+                                .addSchemas(AppSearchEmail.SCHEMA)
+                                .addDocumentClasses(SearchAction.class, ClickAction.class)
+                                .build())
+                .get();
+
+        // Index several email documents
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "email2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+
+        String qualifiedId1 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail1);
+        String qualifiedId2 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail2);
+
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setFetchedResultCount(20)
+                        .build();
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setReferencedQualifiedId(qualifiedId1)
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(1)
+                        .setTimeStayOnResultMillis(512)
+                        .build();
+        ClickAction clickAction2 =
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setReferencedQualifiedId(qualifiedId2)
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(128)
+                        .build();
+        ClickAction clickAction3 =
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */4000)
+                        .setDocumentTtlMillis(0)
+                        .setQuery("body")
+                        .setReferencedQualifiedId(qualifiedId1)
+                        .setResultRankInBlock(2)
+                        .setResultRankGlobal(2)
+                        .setTimeStayOnResultMillis(256)
+                        .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(inEmail1, inEmail2)
+                        .addTakenActions(searchAction, clickAction1, clickAction2, clickAction3)
+                        .build()));
+
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                        .setOrder(SearchSpec.ORDER_DESCENDING)
+                        .addFilterDocumentClasses(ClickAction.class)
+                        .build();
+
+        // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
+        // documents returned. It does not affect the number of child documents that are scored.
+        JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
+                .setNestedSearch("query:body", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .setMaxJoinedResultCount(0)
+                .build();
+
+        // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
+        // query = "body".
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                .build());
+
+        List sr = searchResults.getNextPageAsync().get();
+
+        assertThat(sr).hasSize(2);
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+    }
+// @exportToFramework:endStrip()
+
+    @Test
+    public void testQueryRankByTakenActions_useTakenActionGenericDocument() throws Exception {
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        AppSearchSchema searchActionSchema = new AppSearchSchema.Builder("builtin:SearchAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema clickActionSchema = new AppSearchSchema.Builder("builtin:ClickAction")
+                .addProperty(new LongPropertyConfig.Builder("actionType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build())
+                .addProperty(new StringPropertyConfig.Builder("query")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("referencedQualifiedId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA, searchActionSchema, clickActionSchema)
+                        .build())
+                .get();
+
+        // Index several email documents
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "email1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "email2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .setScore(1)
+                        .build();
+
+        String qualifiedId1 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail1);
+        String qualifiedId2 = DocumentIdUtil.createQualifiedId(
+                mContext.getPackageName(), DB_NAME_1, inEmail2);
+
+        GenericDocument searchAction =
+                new GenericDocument.Builder<>("namespace", "search1", "builtin:SearchAction")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyLong("actionType", ACTION_TYPE_SEARCH)
+                        .setPropertyString("query", "body")
+                        .build();
+        GenericDocument clickAction1 =
+                new GenericDocument.Builder<>("namespace", "click1", "builtin:ClickAction")
+                        .setCreationTimestampMillis(2000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", qualifiedId1)
+                        .build();
+        GenericDocument clickAction2 =
+                new GenericDocument.Builder<>("namespace", "click2", "builtin:ClickAction")
+                        .setCreationTimestampMillis(3000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", qualifiedId2)
+                        .build();
+        GenericDocument clickAction3 =
+                new GenericDocument.Builder<>("namespace", "click3", "builtin:ClickAction")
+                        .setCreationTimestampMillis(4000)
+                        .setPropertyLong("actionType", ACTION_TYPE_CLICK)
+                        .setPropertyString("query", "body")
+                        .setPropertyString("referencedQualifiedId", qualifiedId1)
+                        .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(inEmail1, inEmail2)
+                        .addTakenActionGenericDocuments(
+                                searchAction, clickAction1, clickAction2, clickAction3)
+                        .build()));
+
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE)
+                        .setOrder(SearchSpec.ORDER_DESCENDING)
+                        .addFilterSchemas("builtin:ClickAction")
+                        .build();
+
+        // Note: SearchSpec.Builder#setMaxJoinedResultCount only limits the number of child
+        // documents returned. It does not affect the number of child documents that are scored.
+        JoinSpec js = new JoinSpec.Builder("referencedQualifiedId")
+                .setNestedSearch("query:body", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .setMaxJoinedResultCount(0)
+                .build();
+
+        // Search "body" for AppSearchEmail documents, ranking by ClickAction signals with
+        // query = "body".
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterSchemas(AppSearchEmail.SCHEMA_TYPE)
+                .build());
+
+        List sr = searchResults.getNextPageAsync().get();
+
+        assertThat(sr).hasSize(2);
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("email1");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(2.0);
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("email2");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+    }
+
     @Test
     public void testQuery_invalidAdvancedRanking() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(
@@ -2267,7 +3057,7 @@
         assertThat(documents).hasSize(1);
         assertThat(documents).containsExactly(inDoc);
 
-        // Query only for non-exist type
+        // Query only for non-existent type
         searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .addFilterSchemas("nonExistType")
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
@@ -2353,7 +3143,7 @@
         assertThat(documents).hasSize(1);
         assertThat(documents).containsExactly(expectedEmail);
 
-        // Query only for non-exist namespace
+        // Query only for non-existent namespace
         searchResults = mDb1.search("body",
                 new SearchSpec.Builder()
                         .addFilterNamespaces("nonExistNamespace")
@@ -2699,7 +3489,7 @@
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection(
-                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+                        SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
                 .build());
         List documents = convertSearchResultsToDocuments(searchResults);
 
@@ -2760,7 +3550,7 @@
         // Query with type property paths {"*", []}
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .addProjection(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, Collections.emptyList())
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.emptyList())
                 .build());
         List documents = convertSearchResultsToDocuments(searchResults);
 
@@ -2822,7 +3612,7 @@
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .addProjection("NonExistentType", Collections.emptyList())
                 .addProjection(
-                        SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
+                        SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("body", "to"))
                 .build());
         List documents = convertSearchResultsToDocuments(searchResults);
 
@@ -2842,6 +3632,23 @@
     }
 
     @Test
+    public void testSearchSpec_setSourceTag_notSupported() {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG));
+        // UnsupportedOperationException will be thrown with these queries so no need to
+        // define a schema and index document.
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder.setSearchSourceLogTag("tag").build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("\"Hello, world!\"", searchSpec));
+        assertThat(exception).hasMessageThat().contains(
+                Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG
+                        + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
     public void testQuery_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
@@ -2889,6 +3696,332 @@
     }
 
     @Test
+    public void testQuery_typePropertyFilters() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Query with type property filters {"Email", ["subject", "to"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        // Only email2 should be returned because email1 doesn't have the term "body" in subject
+        // or to fields
+        assertThat(documents).containsExactly(email2);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersWithDifferentSchemaTypes() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"Email": ["subject", "to"], "Note": ["body"]}. Note
+        // schema has body in its property filter but Email schema doesn't.
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .addFilterProperties("Note", ImmutableList.of("body"))
+                .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        // Only the note document should be returned because the email property filter doesn't
+        // allow searching in the body.
+        assertThat(documents).containsExactly(note);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersWithWildcard() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*": ["subject", "title"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "title"))
+                .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        // The wildcard property filter will apply to both the Email and Note schema. The email
+        // document should be returned since it has the term "body" in its subject property. The
+        // note document should not be returned since it doesn't have the term "body" in the title
+        // property (subject property is not applicable for Note schema)
+        assertThat(documents).containsExactly(email);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersWithWildcardAndExplicitSchema() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"*": ["subject", "title"], "Note": ["body"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        ImmutableList.of("subject", "title"))
+                .addFilterProperties("Note", ImmutableList.of("body"))
+                .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        // The wildcard property filter will only apply to the Email schema since Note schema has
+        // its own explicit property filter specified. The email document should be returned since
+        // it has the term "body" in its subject property. The note document should also be returned
+        // since it has the term "body" in the body property.
+        assertThat(documents).containsExactly(email, note);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersNonExistentType() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example subject with some body")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"NonExistentType": ["to", "title"]}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties("NonExistentType", ImmutableList.of("to", "title"))
+                .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        // The supplied property filters don't apply to either schema types. Both the documents
+        // should be returned since the term "body" is present in at least one of their properties.
+        assertThat(documents).containsExactly(email, note);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersEmpty() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .addSchemas(new AppSearchSchema.Builder("Note")
+                                .addProperty(new StringPropertyConfig.Builder("title")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .addProperty(new StringPropertyConfig.Builder("body")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(
+                                                StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                                .build())
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        GenericDocument note =
+                new GenericDocument.Builder<>("namespace", "id2", "Note")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyString("title", "Note title")
+                        .setPropertyString("body", "Note body").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email, note).build()));
+
+        // Query with type property paths {"email": []}
+        SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        // The email document should not be returned since the property filter doesn't allow
+        // searching any property.
+        assertThat(documents).containsExactly(note);
+    }
+
+    @Test
     public void testSnippet() throws Exception {
         // Schema registration
         AppSearchSchema genericSchema = new AppSearchSchema.Builder("Generic")
@@ -2929,10 +4062,10 @@
         assertThat(matchInfo.getFullText()).isEqualTo("A commonly used fake word is foo. "
                 + "Another nonsense word that’s used a lot is bar");
         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/32));
+                new SearchResult.MatchRange(/*start=*/29,  /*end=*/32));
         assertThat(matchInfo.getExactMatch()).isEqualTo("foo");
         assertThat(matchInfo.getSnippetRange()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/26,  /*upper=*/33));
+                new SearchResult.MatchRange(/*start=*/26,  /*end=*/33));
         assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
 
         if (!mDb1.getFeatures().isFeatureSupported(
@@ -2941,7 +4074,7 @@
             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
         } else {
             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/29,  /*upper=*/31));
+                    new SearchResult.MatchRange(/*start=*/29,  /*end=*/31));
             assertThat(matchInfo.getSubmatch()).isEqualTo("fo");
         }
     }
@@ -3068,7 +4201,7 @@
         SearchResult.MatchInfo matchInfo = matchInfos.get(0);
         assertThat(matchInfo.getFullText()).isEqualTo(japanese);
         assertThat(matchInfo.getExactMatchRange()).isEqualTo(
-                new SearchResult.MatchRange(/*lower=*/44,  /*upper=*/45));
+                new SearchResult.MatchRange(/*start=*/44,  /*end=*/45));
         assertThat(matchInfo.getExactMatch()).isEqualTo("は");
 
         if (!mDb1.getFeatures().isFeatureSupported(
@@ -3077,7 +4210,7 @@
             assertThrows(UnsupportedOperationException.class, matchInfo::getSubmatch);
         } else {
             assertThat(matchInfo.getSubmatchRange()).isEqualTo(
-                    new SearchResult.MatchRange(/*lower=*/44,  /*upper=*/45));
+                    new SearchResult.MatchRange(/*start=*/44,  /*end=*/45));
             assertThat(matchInfo.getSubmatch()).isEqualTo("は");
         }
     }
@@ -3992,7 +5125,7 @@
         // returned.
         SearchResults searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                 .build());
         List documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail4);
@@ -4001,7 +5134,7 @@
         // be returned ('email4' and 'email2').
         searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
-                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail4, inEmail2);
@@ -4011,13 +5144,442 @@
         searchResults = mDb1.search("body", new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                 .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                 .build());
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail4, inEmail2);
     }
 
     @Test
+    public void testQuery_ResultGroupingLimits_SchemaGroupingSupported() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
+        // Schema registration
+        AppSearchSchema genericSchema =
+                new AppSearchSchema.Builder("Generic")
+                .addProperty(
+                    new StringPropertyConfig.Builder("foo")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(
+                            StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                    .addSchemas(AppSearchEmail.SCHEMA)
+                    .addSchemas(genericSchema)
+                    .build())
+                .get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "id2")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "id3")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+        AppSearchEmail inEmail5 =
+                new AppSearchEmail.Builder("namespace2", "id5")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail5).build()));
+        GenericDocument inDoc1 =
+                new GenericDocument.Builder<>("namespace3", "id6", "Generic")
+                .setPropertyString("foo", "body")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inDoc1).build()));
+        GenericDocument inDoc2 =
+                new GenericDocument.Builder<>("namespace3", "id7", "Generic")
+                .setPropertyString("foo", "body")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inDoc2).build()));
+        GenericDocument inDoc3 =
+                new GenericDocument.Builder<>("namespace4", "id8", "Generic")
+                .setPropertyString("foo", "body")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inDoc3).build()));
+
+        // Query with per package result grouping. Only the last document 'doc3' should be
+        // returned.
+        SearchResults searchResults =
+                mDb1.search(
+                "body",
+                    new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_PACKAGE, /* limit= */ 1)
+                    .build());
+        List documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3);
+
+        // Query with per namespace result grouping. Only the last document in each namespace should
+        // be returned ('doc3', 'doc2', 'email5' and 'email2').
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per namespace result grouping. Two of the last documents in each namespace
+        // should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4', 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+        // Query with per schema result grouping. Only the last document of each schema type should
+        // be returned ('doc3', 'email5')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inEmail5);
+
+        // Query with per schema result grouping. Only the last two documents of each schema type
+        // should be returned ('doc3', 'doc2', 'email5', 'email4')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
+
+        // Query with per package and per namespace result grouping. Only the last document in each
+        // namespace should be returned ('doc3', 'doc2', 'email5' and 'email2').
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per package and per namespace result grouping. Only the last two documents
+        // in each namespace should be returned ('doc3', 'doc2', 'doc1', 'email5', 'email4',
+        // 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+        // Query with per package and per schema type result grouping. Only the last document in
+        // each schema type should be returned. ('doc3', 'email5')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inEmail5);
+
+        // Query with per package and per schema type result grouping. Only the last two document in
+        // each schema type should be returned. ('doc3', 'doc2', 'email5', 'email4')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail4);
+
+        // Query with per namespace and per schema type result grouping. Only the last document in
+        // each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2').
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per namespace and per schema type result grouping. Only the last two documents
+        // in each namespace should be returned. ('doc3', 'doc2', 'doc1', 'email5', 'email4',
+        // 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+
+        // Query with per namespace, per package and per schema type result grouping. Only the last
+        // document in each namespace should be returned. ('doc3', 'doc2', 'email5' and 'email2')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 1)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents).containsExactly(inDoc3, inDoc2, inEmail5, inEmail2);
+
+        // Query with per namespace, per package and per schema type result grouping. Only the last
+        // two documents in each namespace should be returned.('doc3', 'doc2', 'doc1', 'email5',
+        // 'email4', 'email2', 'email1')
+        searchResults =
+            mDb1.search(
+                "body",
+                new SearchSpec.Builder()
+                    .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                    .setResultGrouping(
+                        SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                            | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                            | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                        /* limit= */ 2)
+                    .build());
+        documents = convertSearchResultsToDocuments(searchResults);
+        assertThat(documents)
+                .containsExactly(inDoc3, inDoc2, inDoc1, inEmail5, inEmail4, inEmail2, inEmail1);
+    }
+
+    @Test
+    public void testQuery_ResultGroupingLimits_SchemaGroupingNotSupported() throws Exception {
+        assumeFalse(
+                mDb1.getFeatures()
+                .isFeatureSupported(Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // Index four documents.
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace1", "id1")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace1", "id2")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+        AppSearchEmail inEmail3 =
+                new AppSearchEmail.Builder("namespace2", "id3")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail3).build()));
+        AppSearchEmail inEmail4 =
+                new AppSearchEmail.Builder("namespace2", "id4")
+                .setFrom("[email protected]")
+                .setTo("[email protected]", "[email protected]")
+                .setSubject("testPut example")
+                .setBody("This is the body of the testPut email")
+                .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                    new PutDocumentsRequest.Builder().addGenericDocuments(inEmail4).build()));
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec1 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_SCHEMA, /* limit= */ 1)
+                .build();
+        UnsupportedOperationException exception =
+                assertThrows(
+                UnsupportedOperationException.class,
+                    () -> mDb1.search("body", searchSpec1));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec2 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_PACKAGE
+                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                    /* limit= */ 1)
+                .build();
+        exception =
+            assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("body", searchSpec2));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec3 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA,
+                    /* limit= */ 1)
+                .build();
+        exception =
+            assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("body", searchSpec3));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+
+        // SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA is not supported.
+        // UnsupportedOperationException will be thrown.
+        SearchSpec searchSpec4 =
+                new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setResultGrouping(
+                    SearchSpec.GROUPING_TYPE_PER_NAMESPACE
+                        | SearchSpec.GROUPING_TYPE_PER_SCHEMA
+                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE,
+                    /* limit= */ 1)
+                .build();
+        exception =
+            assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("body", searchSpec4));
+        assertThat(exception)
+                .hasMessageThat()
+                .contains(
+                    Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA
+                    + " is not available on this"
+                    + " AppSearch implementation.");
+    }
+
+    @Test
     public void testIndexNestedDocuments() throws Exception {
         // Schema registration
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
@@ -4308,8 +5870,6 @@
                 .build();
         mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
 
-        // ListFilterQueryLanguage is enabled so that EXPERIMENTAL_ICING_ADVANCED_QUERY gets enabled
-        // in IcingLib.
         // Disable VERBATIM_SEARCH in the SearchSpec.
         SearchResults searchResults = mDb1.search("\"Hello, world!\"",
                 new SearchSpec.Builder()
@@ -4507,6 +6067,138 @@
     }
 
     @Test
+    public void testQuery_listFilterQueryHasPropertyFunction_notSupported() throws Exception {
+        assumeFalse(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+
+        // UnsupportedOperationException will be thrown with these queries so no need to
+        // define a schema and index document.
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder.setListFilterHasPropertyFunctionEnabled(true).build();
+
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("\"Hello, world!\"", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    public void testQuery_hasPropertyFunction() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
+                .addProperty(new StringPropertyConfig.Builder("prop1")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("prop2")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .setForceOverride(true).addSchemas(schema).build()).get();
+
+        GenericDocument doc1 = new GenericDocument.Builder<>(
+                "namespace", "id1", "Schema")
+                .setPropertyString("prop1", "Hello, world!")
+                .build();
+        GenericDocument doc2 = new GenericDocument.Builder<>(
+                "namespace", "id2", "Schema")
+                .setPropertyString("prop2", "Hello, world!")
+                .build();
+        GenericDocument doc3 = new GenericDocument.Builder<>(
+                "namespace", "id3", "Schema")
+                .setPropertyString("prop1", "Hello, world!")
+                .setPropertyString("prop2", "Hello, world!")
+                .build();
+        mDb1.putAsync(new PutDocumentsRequest.Builder()
+                .addGenericDocuments(doc1, doc2, doc3).build()).get();
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build();
+        SearchResults searchResults = mDb1.search("hasProperty(\"prop1\")",
+                searchSpec);
+        List page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id1");
+
+        searchResults = mDb1.search("hasProperty(\"prop2\")", searchSpec);
+        page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(2);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+        assertThat(page.get(1).getGenericDocument().getId()).isEqualTo("id2");
+
+        searchResults = mDb1.search(
+                "hasProperty(\"prop1\") AND hasProperty(\"prop2\")",
+                searchSpec);
+        page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(1);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("id3");
+    }
+
+    @Test
+    public void testQuery_hasPropertyFunctionWithoutEnablingFeatureFails() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_HAS_PROPERTY_FUNCTION));
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema")
+                .addProperty(new StringPropertyConfig.Builder("prop")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .setForceOverride(true).addSchemas(schema).build()).get();
+
+        GenericDocument doc = new GenericDocument.Builder<>(
+                "namespace1", "id1", "Schema")
+                .setPropertyString("prop", "Hello, world!")
+                .build();
+        mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()).get();
+
+        // Enable LIST_FILTER_HAS_PROPERTY_FUNCTION but disable LIST_FILTER_QUERY_LANGUAGE in the
+        // SearchSpec.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(false)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search("hasProperty(\"prop\")",
+                searchSpec);
+        ExecutionException executionException = assertThrows(ExecutionException.class,
+                () -> searchResults.getNextPageAsync().get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+        assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_QUERY_LANGUAGE);
+
+        // Disable LIST_FILTER_HAS_PROPERTY_FUNCTION in the SearchSpec.
+        searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(false)
+                .build();
+        SearchResults searchResults2 = mDb1.search("hasProperty(\"prop\")",
+                searchSpec);
+        executionException = assertThrows(ExecutionException.class,
+                () -> searchResults2.getNextPageAsync().get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+        assertThat(exception).hasMessageThat().contains("HAS_PROPERTY_FUNCTION");
+    }
+
+    @Test
     public void testQuery_propertyWeightsNotSupported() throws Exception {
         assumeFalse(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
 
@@ -4793,6 +6485,257 @@
     }
 
     @Test
+    public void testQueryWithJoin_typePropertyFiltersOnNestedSpec() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used with property filters in join spec
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("viewType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index 2 email documents
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Index 2 viewAction documents, one for email1 and the other for email2
+        String qualifiedId1 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id1");
+        String qualifiedId2 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id2");
+        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+                .setPropertyString("entityId", qualifiedId1)
+                .setPropertyString("note", "Viewed email on Monday")
+                .setPropertyString("viewType", "Stared").build();
+        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+                .setPropertyString("entityId", qualifiedId2)
+                .setPropertyString("note", "Viewed email on Tuesday")
+                .setPropertyString("viewType", "Viewed").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+                                viewAction1, viewAction2)
+                        .build()));
+
+        // The nested search spec only allows searching the viewType property for viewAction
+        // schema type. It also specifies a property filter for Email schema.
+        SearchSpec nestedSearchSpec =
+                new SearchSpec.Builder()
+                        .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+                        .addFilterProperties(AppSearchEmail.SCHEMA_TYPE,
+                                ImmutableList.of("subject"))
+                        .build();
+
+        // Search for the term "Viewed" in join spec
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("Viewed", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .build());
+
+        List sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned, email2 comes first because it has higher number of
+        // joined documents. The property filters for Email schema specified in the nested search
+        // specs don't apply to the outer query (otherwise none of the email documents would have
+        // been returned).
+        assertThat(sr).hasSize(2);
+
+        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+        // the join spec, so it should be present in the joined results.
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+        // Email1 has a viewAction document viewAction1 but it doesn't satisfy the property filters
+        // in the join spec, so it should not be present in the joined results.
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(0.0);
+        assertThat(sr.get(1).getJoinedResults()).isEmpty();
+    }
+
+    @Test
+    public void testQueryWithJoin_typePropertyFiltersOnOuterSpec() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        assumeTrue(mDb1.getFeatures()
+                .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
+
+        // A full example of how join might be used with property filters in join spec
+        AppSearchSchema actionSchema = new AppSearchSchema.Builder("ViewAction")
+                .addProperty(new StringPropertyConfig.Builder("entityId")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setJoinableValueType(StringPropertyConfig
+                                .JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("note")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new StringPropertyConfig.Builder("viewType")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA, actionSchema)
+                        .build()).get();
+
+        // Index 2 email documents
+        AppSearchEmail inEmail =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setFrom("[email protected]")
+                        .setTo("[email protected]", "[email protected]")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+
+        // Index 2 viewAction documents, one for email1 and the other for email2
+        String qualifiedId1 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id1");
+        String qualifiedId2 =
+                DocumentIdUtil.createQualifiedId(
+                        ApplicationProvider.getApplicationContext().getPackageName(), DB_NAME_1,
+                        "namespace", "id2");
+        GenericDocument viewAction1 = new GenericDocument.Builder<>("NS", "id3", "ViewAction")
+                .setPropertyString("entityId", qualifiedId1)
+                .setPropertyString("note", "Viewed email on Monday")
+                .setPropertyString("viewType", "Stared").build();
+        GenericDocument viewAction2 = new GenericDocument.Builder<>("NS", "id4", "ViewAction")
+                .setPropertyString("entityId", qualifiedId2)
+                .setPropertyString("note", "Viewed email on Tuesday")
+                .setPropertyString("viewType", "Viewed").build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(inEmail, inEmail2,
+                                viewAction1, viewAction2)
+                        .build()));
+
+        // The nested search spec doesn't specify any property filters.
+        SearchSpec nestedSearchSpec = new SearchSpec.Builder().build();
+
+        // Search for the term "Viewed" in join spec
+        JoinSpec js = new JoinSpec.Builder("entityId")
+                .setNestedSearch("Viewed", nestedSearchSpec)
+                .setAggregationScoringStrategy(JoinSpec.AGGREGATION_SCORING_RESULT_COUNT)
+                .build();
+
+        // Outer search spec adds property filters for both Email and ViewAction schema
+        SearchResults searchResults = mDb1.search("body email", new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE)
+                .setJoinSpec(js)
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("body"))
+                .addFilterProperties("ViewAction", ImmutableList.of("viewType"))
+                .build());
+
+        List sr = searchResults.getNextPageAsync().get();
+
+        // Both email docs are returned as they both satisfy the property filters for Email, email2
+        // comes first because it has higher id lexicographically.
+        assertThat(sr).hasSize(2);
+
+        // Email2 has a viewAction document viewAction2 that satisfies the property filters in
+        // the outer spec (although those property filters are irrelevant for joined documents),
+        // it should be present in the joined results.
+        assertThat(sr.get(0).getGenericDocument().getId()).isEqualTo("id2");
+        assertThat(sr.get(0).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(0).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction2);
+
+        // Email1 has a viewAction document viewAction1 that doesn't satisfy the property filters
+        // in the outer spec, but property filters in the outer spec should not apply on joined
+        // documents, so viewAction1 should be present in the joined results.
+        assertThat(sr.get(1).getGenericDocument().getId()).isEqualTo("id1");
+        assertThat(sr.get(1).getRankingSignal()).isEqualTo(1.0);
+        assertThat(sr.get(0).getJoinedResults()).hasSize(1);
+        assertThat(sr.get(1).getJoinedResults().get(0).getGenericDocument()).isEqualTo(viewAction1);
+    }
+
+    @Test
+    public void testQuery_typePropertyFiltersNotSupported() throws Exception {
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Query with type property filters {"Email", ["subject", "to"]} and verify that unsupported
+        // exception is thrown
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addFilterProperties(AppSearchEmail.SCHEMA_TYPE, ImmutableList.of("subject", "to"))
+                .build();
+        UnsupportedOperationException exception =
+                assertThrows(UnsupportedOperationException.class,
+                        () -> mDb1.search("body", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
     public void testSimpleJoin() throws Exception {
         assumeTrue(mDb1.getFeatures()
                 .isFeatureSupported(Features.JOIN_SPEC_AND_QUALIFIED_ID));
@@ -4915,7 +6858,7 @@
         assertThrows(UnsupportedOperationException.class, () ->
                 mDb1.searchSuggestionAsync(
                         /*suggestionQueryExpression=*/"t",
-                        new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get());
+                        new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get());
     }
 
     @Test
@@ -4960,14 +6903,14 @@
 
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultTwo, resultThree, resultFour)
                 .inOrder();
 
         // Query first 2 suggestions, and they will be ranked.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/2).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/2).build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
     }
 
@@ -5008,21 +6951,21 @@
         // namespace1 has 2 results.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1").build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
 
         // namespace2 has 1 result.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace2").build()).get();
         assertThat(suggestions).containsExactly(resultFoo).inOrder();
 
         // namespace2 and 3 has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace2", "namespace3")
                         .build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFool);
@@ -5030,7 +6973,7 @@
         // non exist namespace has empty result
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("nonExistNamespace").build()).get();
         assertThat(suggestions).isEmpty();
     }
@@ -5077,7 +7020,7 @@
         // Only search for namespace1/doc1
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1")
                         .addFilterDocumentIds("namespace1", "id1")
                         .build()).get();
@@ -5086,7 +7029,7 @@
         // Only search for namespace1/doc1 and namespace1/doc2
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1")
                         .addFilterDocumentIds("namespace1", ImmutableList.of("id1", "id2"))
                         .build()).get();
@@ -5095,7 +7038,7 @@
         // Only search for namespace1/doc1 and namespace2/doc3
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterNamespaces("namespace1", "namespace2")
                         .addFilterDocumentIds("namespace1", "id1")
                         .addFilterDocumentIds("namespace2", ImmutableList.of("id3"))
@@ -5105,7 +7048,7 @@
         // Only search for namespace1/doc1 and everything in namespace2
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterDocumentIds("namespace1", "id1")
                         .build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
@@ -5163,21 +7106,21 @@
         // Type1 has 2 results.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("Type1").build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFo).inOrder();
 
         // Type2 has 1 result.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("Type2").build()).get();
         assertThat(suggestions).containsExactly(resultFoo).inOrder();
 
         // Type2 and 3 has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("Type2", "Type3")
                         .build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFool);
@@ -5185,7 +7128,7 @@
         // non exist type has empty result.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .addFilterSchemas("nonExistType").build()).get();
         assertThat(suggestions).isEmpty();
     }
@@ -5233,13 +7176,13 @@
         // prefix f has 2 results.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultFoo, resultFool);
 
         // prefix b has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"b",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultBar, resultBaz);
     }
 
@@ -5285,7 +7228,7 @@
         // rank by NONE, the order should be arbitrary but all terms appear.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .setRankingStrategy(SearchSuggestionSpec
                                 .SUGGESTION_RANKING_STRATEGY_NONE)
                         .build()).get();
@@ -5294,7 +7237,7 @@
         // rank by document count, the order should be term1:3 > term2:2 > term3:1
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .setRankingStrategy(SearchSuggestionSpec
                                 .SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT)
                         .build()).get();
@@ -5303,7 +7246,7 @@
         // rank by term frequency, the order should be term3:5 > term2:4 > term1:3
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10)
                         .setRankingStrategy(SearchSuggestionSpec
                                 .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
                         .build()).get();
@@ -5348,7 +7291,7 @@
         // prefix t has 3 results.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
 
         // Delete the document
@@ -5359,7 +7302,7 @@
         // now prefix t has 2 results.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultThree, resultTart);
     }
 
@@ -5397,7 +7340,7 @@
         // prefix t has 3 results.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultTwo, resultThree, resultTart);
 
         // replace the document
@@ -5411,7 +7354,7 @@
         // prefix t has 2 results for now.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultThree, resultTwist);
     }
 
@@ -5448,13 +7391,13 @@
         // database 1 could get suggestion results
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).containsExactly(resultOne, resultTwo).inOrder();
 
         // database 2 couldn't get suggestion results
         suggestions = mDb2.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"t",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         assertThat(suggestions).isEmpty();
     }
 
@@ -5488,7 +7431,7 @@
         // Search "bar AND f" only document 1 should match the search.
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"bar f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("bar fo").build();
         assertThat(suggestions).containsExactly(barFo);
@@ -5497,7 +7440,7 @@
         // match.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"bar OR cat f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barCatFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("bar OR cat fo").build();
         SearchSuggestionResult barCatFoo =
@@ -5507,7 +7450,7 @@
         // Search for "(bar AND cat) OR f", all documents could match.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"(bar cat) OR f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barCatOrFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("(bar cat) OR fo").build();
         SearchSuggestionResult barCatOrFoo =
@@ -5520,7 +7463,7 @@
         // Search for "-bar f", document2 "cat foo" could and document3 "fool" could match.
         suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"-bar f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult noBarFoo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("-bar foo").build();
         SearchSuggestionResult noBarFool =
@@ -5529,6 +7472,162 @@
     }
 
     @Test
+    public void testSearchSuggestion_propertyFilter() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+        // Schema registration
+        AppSearchSchema schemaType1 =
+                new AppSearchSchema.Builder("Type1")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertyone")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertytwo")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        AppSearchSchema schemaType2 =
+                new AppSearchSchema.Builder("Type2")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertythree")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("propertyfour")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .build())
+                        .build();
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder().addSchemas(schemaType1, schemaType2).build())
+                .get();
+
+        // Index documents
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Type1")
+                        .setPropertyString("propertyone", "termone")
+                        .setPropertyString("propertytwo", "termtwo")
+                        .build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "id2", "Type2")
+                        .setPropertyString("propertythree", "termthree")
+                        .setPropertyString("propertyfour", "termfour")
+                        .build();
+
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2).build()));
+
+        SearchSuggestionResult resultOne =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termone").build();
+        SearchSuggestionResult resultTwo =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termtwo").build();
+        SearchSuggestionResult resultThree =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termthree").build();
+        SearchSuggestionResult resultFour =
+                new SearchSuggestionResult.Builder().setSuggestedResult("termfour").build();
+
+        // Only search for type1/propertyone
+        List suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1")
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne);
+
+        // Only search for type1/propertyone and type1/propertytwo
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1")
+                                        .addFilterProperties(
+                                                "Type1",
+                                                ImmutableList.of("propertyone", "propertytwo"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultTwo);
+
+        // Only search for type1/propertyone and type2/propertythree
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1", "Type2")
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .addFilterProperties(
+                                                "Type2", ImmutableList.of("propertythree"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultThree);
+
+        // Only search for type1/propertyone and type2/propertyfour, in addFilterPropertyPaths
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterSchemas("Type1", "Type2")
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .addFilterPropertyPaths(
+                                                "Type2",
+                                                ImmutableList.of(new PropertyPath("propertyfour")))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultFour);
+
+        // Only search for type1/propertyone and everything in type2
+        suggestions =
+                mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t",
+                                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                                        .addFilterProperties(
+                                                "Type1", ImmutableList.of("propertyone"))
+                                        .build())
+                        .get();
+        assertThat(suggestions).containsExactly(resultOne, resultThree, resultFour);
+    }
+
+    @Test
+    public void testSearchSuggestion_propertyFilter_notSupported() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES));
+
+        SearchSuggestionSpec searchSuggestionSpec =
+                new SearchSuggestionSpec.Builder(/* maximumResultCount= */ 10)
+                    .addFilterSchemas("Type1")
+                    .addFilterProperties("Type1", ImmutableList.of("property"))
+                    .build();
+
+        // Search suggest with type property filters {"Email", ["property"]} and verify that
+        // unsupported exception is thrown
+        UnsupportedOperationException exception =
+                assertThrows(UnsupportedOperationException.class,
+                        () -> mDb1.searchSuggestionAsync(
+                                /* suggestionQueryExpression= */ "t", searchSuggestionSpec).get());
+        assertThat(exception).hasMessageThat().contains(Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
     public void testSearchSuggestion_PropertyRestriction() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SUGGESTION));
         // Schema registration
@@ -5566,7 +7665,7 @@
         // Search for "bar AND subject:f"
         List suggestions = mDb1.searchSuggestionAsync(
                 /*suggestionQueryExpression=*/"bar subject:f",
-                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10).build()).get();
+                new SearchSuggestionSpec.Builder(/*maximumResultCount=*/10).build()).get();
         SearchSuggestionResult barSubjectFo =
                 new SearchSuggestionResult.Builder().setSuggestedResult("bar subject:fo").build();
         SearchSuggestionResult barSubjectFoo =
@@ -5609,6 +7708,13 @@
         Set actual = mDb1.getSchemaAsync().get().getSchemas();
         assertThat(actual).hasSize(3);
         assertThat(actual).isEqualTo(request.getSchemas());
+
+        // Check that calling getParentType() for the EmailMessage schema returns Email and Message
+        for (AppSearchSchema schema : actual) {
+            if (schema.getSchemaType().equals("EmailMessage")) {
+                assertThat(schema.getParentTypes()).containsExactly("Email", "Message");
+            }
+        }
     }
 
     @Test
@@ -5641,6 +7747,70 @@
     }
 
     @Test
+    public void testGetSchema_indexableNestedPropsList() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES));
+
+        AppSearchSchema personSchema =
+                new AppSearchSchema.Builder("Person")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder(
+                                        "worksFor", "Organization")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setShouldIndexNestedProperties(false)
+                                        .addIndexableNestedProperties(Collections.singleton("name"))
+                                        .build())
+                        .build();
+        AppSearchSchema organizationSchema =
+                new AppSearchSchema.Builder("Organization")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("name")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .addProperty(
+                                new StringPropertyConfig.Builder("notes")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .setIndexingType(
+                                                StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                        .build())
+                        .build();
+
+        SetSchemaRequest setSchemaRequest =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(personSchema, organizationSchema)
+                        .build();
+        mDb1.setSchemaAsync(setSchemaRequest).get();
+
+        Set actual = mDb1.getSchemaAsync().get().getSchemas();
+        assertThat(actual).hasSize(2);
+        assertThat(actual).isEqualTo(setSchemaRequest.getSchemas());
+
+        for (AppSearchSchema schema : actual) {
+            if (schema.getSchemaType().equals("Person")) {
+                for (PropertyConfig property : schema.getProperties()) {
+                    if (property.getName().equals("worksFor")) {
+                        assertThat(
+                                ((DocumentPropertyConfig) property)
+                                        .getIndexableNestedProperties()).containsExactly("name");
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
     public void testSetSchema_dataTypeIncompatibleWithParentTypes() throws Exception {
         assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_ADD_PARENT_TYPE));
         AppSearchSchema messageSchema =
@@ -6473,4 +8643,643 @@
         assertThat(emailSchema.toString()).contains(expectedIndexableNestedPropertyMessage);
 
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_simple() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+
+        // Match documents that have embeddings with a similarity closer to 0 that is
+        // greater than -1.
+        //
+        // The matched embeddings for each doc are:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({-0.5, 0.3}) + sum({}) = -0.2
+        // - document 1: sum({-0.9}) + sum({}) = -0.9
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy(
+                        "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+        List results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.2);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_propertyRestriction() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+
+        // Create a query similar as above but with a property restriction, which still matches
+        // document 0 and document 1 but the semantic score 0.3 should be removed from document 0.
+        //
+        // The matched embeddings for each doc are:
+        // - document 0: -0.5 (embedding1)
+        // - document 1: -0.9 (embedding1)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({-0.5}) = -0.5
+        // - document 1: sum({-0.9}) = -0.9
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "embedding1:semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+        List results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.5);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_multipleSearchEmbeddings() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2)
+        // - document 1: -0.9 (embedding1)
+        EmbeddingVector searchEmbedding1 = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding2)
+        // - document 1: -2.1 (embedding2)
+        EmbeddingVector searchEmbedding2 = new EmbeddingVector(
+                new float[]{-1, -1, 1}, "my_model_v2");
+
+        // Create a complex query that matches all hits from all documents.
+        //
+        // The matched embeddings for each doc are:
+        // - document 0: -0.5 (embedding1), 0.3 (embedding2), -0.5 (embedding2)
+        // - document 1: -0.9 (embedding1), -2.1 (embedding2)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({-0.5, 0.3}) + sum({-0.5}) = -0.7
+        // - document 1: sum({-0.9}) + sum({-2.1}) = -3
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding1, searchEmbedding2)
+                .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0))) + "
+                        + "sum(this.matchedSemanticScores(getSearchSpecEmbedding(1)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0)) OR "
+                        + "semanticSearch(getSearchSpecEmbedding(1))", searchSpec);
+        List results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(-0.7);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-3);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_hybrid() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                new float[]{0.6f, 0.7f, -0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: -0.5 (embedding2)
+        // - document 1: -2.1 (embedding2)
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{-1, -1, 1}, "my_model_v2");
+
+        // Create a hybrid query that matches document 0 because of term-based search
+        // and document 1 because of embedding-based search.
+        //
+        // The matched embeddings for each doc are:
+        // - document 1: -2.1 (embedding2)
+        // The scoring expression for each doc will be evaluated as:
+        // - document 0: sum({}) = 0
+        // - document 1: sum({-2.1}) = -2.1
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy("sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "foo OR semanticSearch(getSearchSpecEmbedding(0), -10, -1)", searchSpec);
+        List results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-2.1);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearchWithoutEnablingFeatureFails() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding1")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding2")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding1", new EmbeddingVector(
+                                new float[]{0.1f, 0.2f, 0.3f, 0.4f, 0.5f}, "my_model_v1"))
+                        .setPropertyEmbedding("embedding2", new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, 0.4f, 0.5f},
+                                        "my_model_v1"),
+                                new EmbeddingVector(
+                                        new float[]{0.6f, 0.7f, 0.8f}, "my_model_v2"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc).build()));
+
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model_v1");
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy(
+                        "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .setListFilterQueryLanguageEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec);
+        ExecutionException executionException = assertThrows(ExecutionException.class,
+                () -> searchResults.getNextPageAsync().get());
+        assertThat(executionException).hasCauseThat().isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) executionException.getCause();
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_ARGUMENT);
+        assertThat(exception).hasMessageThat().contains("Attempted use of unenabled feature");
+        assertThat(exception).hasMessageThat().contains("EMBEDDING_SEARCH");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch_notSupported() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeFalse(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("semanticSearch(getSearchSpecEmbedding(0), -1, 1)", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testTokenizeSearch_simple() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_TOKENIZE_FUNCTION));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("body")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setPropertyString("body", "foo bar")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setPropertyString("body", "bar")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "id2", "Email")
+                        .setPropertyString("body", "foo")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1, doc2).build()));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search("tokenize(\"foo.\")", searchSpec);
+        List results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc2, doc0);
+
+        searchResults = mDb1.search("tokenize(\"bar, foo\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+
+        searchResults = mDb1.search("tokenize(\"\\\"bar, \\\"foo\\\"\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+
+        searchResults = mDb1.search("tokenize(\"bar ) foo\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+
+        searchResults = mDb1.search("tokenize(\"bar foo(\")", searchSpec);
+        results = convertSearchResultsToDocuments(searchResults);
+        assertThat(results).containsExactly(doc0);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testTokenizeSearch_notSupported() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_QUERY_LANGUAGE));
+        assumeFalse(
+                mDb1.getFeatures().isFeatureSupported(Features.LIST_FILTER_TOKENIZE_FUNCTION));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("tokenize(\"foo.\")", searchSpec));
+        assertThat(exception).hasMessageThat().contains(Features.LIST_FILTER_TOKENIZE_FUNCTION
+                + " is not available on this AppSearch implementation.");
+    }
+
+    @Test
+    @RequiresFlagsEnabled({
+            Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS,
+            Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG})
+    public void testInformationalRankingExpressions() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures().isFeatureSupported(Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG));
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
+
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Email")
+                .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
+                        .setCardinality(PropertyConfig.CARDINALITY_REPEATED)
+                        .setIndexingType(
+                                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        final int doc0DocScore = 2;
+        GenericDocument doc0 =
+                new GenericDocument.Builder<>("namespace", "id0", "Email")
+                        .setScore(doc0DocScore)
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding", new EmbeddingVector(
+                                new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
+                        .build();
+        final int doc1DocScore = 3;
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "Email")
+                        .setScore(doc1DocScore)
+                        .setCreationTimestampMillis(1000)
+                        .setPropertyEmbedding("embedding", new EmbeddingVector(
+                                        new float[]{-0.1f, 0.2f, -0.3f, -0.4f, 0.5f}, "my_model"),
+                                new EmbeddingVector(
+                                        new float[]{-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, "my_model"))
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc0, doc1).build()));
+
+        // Add an embedding search with dot product semantic scores:
+        // - document 0: 0.5
+        // - document 1: -0.9, 0.5
+        EmbeddingVector searchEmbedding = new EmbeddingVector(
+                new float[]{1, -1, -1, 1, -1}, "my_model");
+
+        // Make an embedding query that matches all documents.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(searchEmbedding)
+                .setRankingStrategy(
+                        "sum(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .addInformationalRankingExpressions(
+                        "len(this.matchedSemanticScores(getSearchSpecEmbedding(0)))")
+                .addInformationalRankingExpressions("this.documentScore()")
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        SearchResults searchResults = mDb1.search(
+                "semanticSearch(getSearchSpecEmbedding(0))", searchSpec);
+        List results = retrieveAllSearchResults(searchResults);
+        assertThat(results).hasSize(2);
+        // doc0:
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc0);
+        assertThat(results.get(0).getRankingSignal()).isWithin(0.00001).of(0.5);
+        // doc0 has 1 embedding vector and a document score of 2.
+        assertThat(results.get(0).getInformationalRankingSignals())
+                .containsExactly(1.0, (double) doc0DocScore).inOrder();
+
+        // doc1:
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getRankingSignal()).isWithin(0.00001).of(-0.9 + 0.5);
+        // doc1 has 2 embedding vectors and a document score of 3.
+        assertThat(results.get(1).getInformationalRankingSignals())
+                .containsExactly(2.0, (double) doc1DocScore).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testInformationalRankingExpressions_notSupported() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION));
+        assumeFalse(mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS));
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setRankingStrategy("this.documentScore() + 1")
+                .addInformationalRankingExpressions("this.documentScore()")
+                .build();
+        UnsupportedOperationException exception = assertThrows(
+                UnsupportedOperationException.class,
+                () -> mDb1.search("foo", searchSpec));
+        assertThat(exception).hasMessageThat().contains(
+                Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS
+                + " are not available on this AppSearch implementation.");
+    }
+
+    @Test
+    public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
+                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .build())
+                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
+                        "document", AppSearchEmail.SCHEMA_TYPE)
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
+                        .setShouldIndexNestedProperties(true)
+                        .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
+
+        // Index a document
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
+                .setPropertyBytes("bytes")
+                .setPropertyDocument("document")
+                .build();
+
+        AppSearchBatchResult result = checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
+        assertThat(result.getSuccesses()).containsExactly("id1", null);
+        assertThat(result.getFailures()).isEmpty();
+
+        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
+                .addIds("id1")
+                .build();
+        List outDocuments = doGet(mDb1, request);
+        assertThat(outDocuments).hasSize(1);
+        GenericDocument outDocument = outDocuments.get(0);
+        assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
+        assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
index aba266a..79e7ad3 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
@@ -20,7 +20,6 @@
 
 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
-import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -34,7 +33,6 @@
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.SearchResult;
@@ -560,44 +558,4 @@
         assertThat(result.getFailures().get("id1").getErrorMessage())
                 .contains("was too large to write. Max is 16777215");
     }
-
-    @Test
-    public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db = LocalStorage.createSearchSessionAsync(
-                new LocalStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
-        // Schema registration
-        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
-                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
-                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                        "document", AppSearchEmail.SCHEMA_TYPE)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setShouldIndexNestedProperties(true)
-                        .build())
-                .build();
-        db.setSchemaAsync(new SetSchemaRequest.Builder()
-                .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document
-        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
-                .setPropertyBytes("bytes")
-                .setPropertyDocument("document")
-                .build();
-
-        AppSearchBatchResult result = checkIsBatchResultSuccess(db.putAsync(
-                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
-        assertThat(result.getSuccesses()).containsExactly("id1", null);
-        assertThat(result.getFailures()).isEmpty();
-
-        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
-                .addIds("id1")
-                .build();
-        List outDocuments = doGet(db, request);
-        assertThat(outDocuments).hasSize(1);
-        GenericDocument outDocument = outDocuments.get(0);
-        assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
-        assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
-    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
index 0fdd512..f2fc0c2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
@@ -16,9 +16,6 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.cts.app;
 
-import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
-import static androidx.appsearch.testutil.AppSearchTestUtils.doGet;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assume.assumeTrue;
@@ -30,16 +27,9 @@
 import android.os.Build;
 
 import androidx.annotation.NonNull;
-import androidx.appsearch.app.AppSearchBatchResult;
-import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.app.Features;
-import androidx.appsearch.app.GenericDocument;
-import androidx.appsearch.app.GetByDocumentIdRequest;
-import androidx.appsearch.app.PutDocumentsRequest;
-import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.platformstorage.PlatformStorage;
-import androidx.appsearch.testutil.AppSearchEmail;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SdkSuppress;
 
@@ -93,51 +83,11 @@
     }
 
     @Test
+    @Override
     public void testPutDocuments_emptyBytesAndDocuments() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-        AppSearchSession db = PlatformStorage.createSearchSessionAsync(
-                new PlatformStorage.SearchContext.Builder(context, DB_NAME_1).build()).get();
-        // Schema registration
-        AppSearchSchema schema = new AppSearchSchema.Builder("testSchema")
-                .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytes")
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .build())
-                .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
-                        "document", AppSearchEmail.SCHEMA_TYPE)
-                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
-                        .setShouldIndexNestedProperties(true)
-                        .build())
-                .build();
-        db.setSchemaAsync(new SetSchemaRequest.Builder()
-                .addSchemas(schema, AppSearchEmail.SCHEMA).build()).get();
-
-        // Index a document
-        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "testSchema")
-                .setPropertyBytes("bytes")
-                .setPropertyDocument("document")
-                .build();
-
-        AppSearchBatchResult result = checkIsBatchResultSuccess(db.putAsync(
-                new PutDocumentsRequest.Builder().addGenericDocuments(document).build()));
-        assertThat(result.getSuccesses()).containsExactly("id1", null);
-        assertThat(result.getFailures()).isEmpty();
-
-        GetByDocumentIdRequest request = new GetByDocumentIdRequest.Builder("namespace")
-                .addIds("id1")
-                .build();
-        List outDocuments = doGet(db, request);
-        assertThat(outDocuments).hasSize(1);
-        GenericDocument outDocument = outDocuments.get(0);
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S
-                || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) {
-            // We fixed b/204677124 in Android T, so in S and S_V2, getByteArray and
-            // getDocumentArray will return null if we set empty properties.
-            assertThat(outDocument.getPropertyBytesArray("bytes")).isNull();
-            assertThat(outDocument.getPropertyDocumentArray("document")).isNull();
-        } else {
-            assertThat(outDocument.getPropertyBytesArray("bytes")).isEmpty();
-            assertThat(outDocument.getPropertyDocumentArray("document")).isEmpty();
-        }
+        // b/185441119 was fixed in Android T, this test will fail on S_V2 devices and below.
+        assumeTrue(Build.VERSION.SDK_INT >= 33);
+        super.testPutDocuments_emptyBytesAndDocuments();
     }
 
     @Override
@@ -202,5 +152,4 @@
     @Override
     @Test
     public void testQuery_advancedRankingWithJoin() throws Exception { }
-
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
index 1d70497..b50d3fa 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GenericDocumentCtsTest.java
@@ -20,13 +20,25 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 
+import org.junit.Rule;
 import org.junit.Test;
 
+import java.util.Objects;
+
 public class GenericDocumentCtsTest {
     private static final byte[] sByteArray1 = new byte[]{(byte) 1, (byte) 2, (byte) 3};
     private static final byte[] sByteArray2 = new byte[]{(byte) 4, (byte) 5, (byte) 6, (byte) 7};
+    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 sDocumentProperties1 = new GenericDocument
             .Builder<>("namespace", "sDocumentProperties1", "sDocumentPropertiesSchemaType1")
             .setCreationTimestampMillis(12345L)
@@ -36,6 +48,9 @@
             .setCreationTimestampMillis(6789L)
             .build();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     @SuppressWarnings("deprecation")
     public void testMaxIndexedProperties() {
@@ -364,9 +379,31 @@
                 () -> builder.setPropertyString("testKey", "string1", nullString));
     }
 
-// @exportToFramework:startStrip()
+    @Test
+    public void testDocumentInvalid_setNullByteValues() {
+        GenericDocument.Builder builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        byte[] nullBytes = null;
 
-    // TODO(b/171882200): Expose this test in Android T
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setPropertyBytes("propBytes", new byte[][]{{1, 2}, nullBytes}));
+    }
+
+    @Test
+    public void testDocumentInvalid_setNullDocValues() {
+        GenericDocument.Builder builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        GenericDocument doc = new GenericDocument.Builder<>("namespace",
+                "id2",
+                "schemaType2").build();
+        GenericDocument nullDoc = null;
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> builder.setPropertyDocument("propDocs", doc, nullDoc));
+    }
+
     @Test
     public void testDocument_toBuilder() {
         GenericDocument document1 = new GenericDocument.Builder<>(
@@ -381,12 +418,13 @@
                 .build();
         GenericDocument document2 =
                 new GenericDocument.Builder<>(document1)
-                .setId("id2")
-                .setNamespace("namespace2")
-                .setPropertyBytes("byteKey1", sByteArray2)
-                .setPropertyLong("longKey2", 10L)
-                .clearProperty("booleanKey1")
-                .build();
+                        .setId("id2")
+                        .setNamespace("namespace2")
+                        .setSchemaType("schemaType2")
+                        .setPropertyBytes("byteKey1", sByteArray2)
+                        .setPropertyLong("longKey2", 10L)
+                        .clearProperty("booleanKey1")
+                        .build();
 
         // Make sure old doc hasn't changed
         assertThat(document1.getId()).isEqualTo("id1");
@@ -399,7 +437,7 @@
 
         // Make sure the new doc contains the expected values
         GenericDocument expectedDoc = new GenericDocument.Builder<>(
-                "namespace2", "id2", "schemaType1")
+                "namespace2", "id2", "schemaType2")
                 .setCreationTimestampMillis(5L)
                 .setPropertyLong("longKey1", 1L, 2L, 3L)
                 .setPropertyLong("longKey2", 10L)
@@ -411,7 +449,51 @@
         assertThat(document2).isEqualTo(expectedDoc);
     }
 
-// @exportToFramework:endStrip()
+    @Test
+    public void testDocument_toBuilder_doesNotModifyOriginal() {
+        GenericDocument oldDoc = new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                .setScore(42)
+                .setPropertyString("propString", "Hello")
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id2", "schema2")
+                                .setPropertyString("propString", "Goodbye")
+                                .setPropertyBytes("propBytes", new byte[][]{{3, 4}})
+                                .build())
+                .build();
+
+        GenericDocument newDoc = new GenericDocument.Builder<>(oldDoc)
+                .setPropertyBytes("propBytes", new byte[][]{{1, 2}})
+                .setPropertyDocument(
+                        "propDocument",
+                        new GenericDocument.Builder<>("namespace", "id3", "schema3")
+                                .setPropertyString("propString", "Bye")
+                                .setPropertyBytes("propBytes", new byte[][]{{5, 6}})
+                                .build())
+                .build();
+
+        // Check that the original GenericDocument is unmodified.
+        assertThat(oldDoc.getScore()).isEqualTo(42);
+        assertThat(oldDoc.getPropertyString("propString")).isEqualTo("Hello");
+        assertThat(oldDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Goodbye");
+        assertThat(oldDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{3, 4}});
+
+        // Check that the new GenericDocument has modified the original fields correctly.
+        assertThat(newDoc.getPropertyBytesArray("propBytes")).isEqualTo(new byte[][]{{1, 2}});
+        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyString("propString"))
+                .isEqualTo("Bye");
+        assertThat(newDoc.getPropertyDocument("propDocument").getPropertyBytesArray("propBytes"))
+                .isEqualTo(new byte[][]{{5, 6}});
+
+        // Check that the new GenericDocument copies fields that aren't set.
+        assertThat(oldDoc.getScore()).isEqualTo(newDoc.getScore());
+        assertThat(oldDoc.getPropertyString("propString")).isEqualTo(newDoc.getPropertyString(
+                "propString"));
+    }
 
     @Test
     public void testRetrieveTopLevelProperties() {
@@ -886,4 +968,230 @@
         assertThat(documents[1].getPropertyNames()).containsExactly("propString", "propInts",
                 "propIntsTwo");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentEquals_identicalWithEmbeddingValues() {
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                .build();
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentEquals_differentOrderWithEmbeddingValues() {
+        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .build();
+
+        // Create second document with same parameter but different order.
+        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                .build();
+        assertThat(document1).isEqualTo(document2);
+        assertThat(document1.hashCode()).isEqualTo(document2.hashCode());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentGetEmbeddingValue() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyDouble("doubleKey1", 1.0)
+                .setPropertyBoolean("booleanKey1", true)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1)
+                .build();
+        assertThat(document.getId()).isEqualTo("id1");
+        assertThat(document.getTtlMillis()).isEqualTo(1L);
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(5);
+        assertThat(document.getScore()).isEqualTo(1);
+        assertThat(document.getPropertyLong("longKey1")).isEqualTo(1L);
+        assertThat(document.getPropertyDouble("doubleKey1")).isEqualTo(1.0);
+        assertThat(document.getPropertyBoolean("booleanKey1")).isTrue();
+        assertThat(document.getPropertyString("stringKey1")).isEqualTo("test-value1");
+        assertThat(Objects.requireNonNull(document.getPropertyEmbedding(
+                "embeddingKey1")).getValues()).usingExactEquality()
+                .containsExactly(1.1f, 2.2f, 3.3f).inOrder();
+        assertThat(Objects.requireNonNull(
+                document.getPropertyEmbedding("embeddingKey1")).getModelSignature()).isEqualTo(
+                "my_model_v1");
+
+        assertThat(document.getProperty("longKey1")).isInstanceOf(long[].class);
+        assertThat((long[]) document.getProperty("longKey1")).asList().containsExactly(1L);
+        assertThat(document.getProperty("doubleKey1")).isInstanceOf(double[].class);
+        assertThat((double[]) document.getProperty("doubleKey1")).usingTolerance(
+                0.05).containsExactly(1.0);
+        assertThat(document.getProperty("booleanKey1")).isInstanceOf(boolean[].class);
+        assertThat((boolean[]) document.getProperty("booleanKey1")).asList().containsExactly(true);
+        assertThat(document.getProperty("stringKey1")).isInstanceOf(String[].class);
+        assertThat((String[]) document.getProperty("stringKey1")).asList().containsExactly(
+                "test-value1");
+        assertThat((EmbeddingVector[]) document.getProperty(
+                "embeddingKey1")).asList().containsExactly(sEmbedding1).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentGetArrayEmbeddingValues() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "test-value1", "test-value2", "test-value3")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                .build();
+
+        assertThat(document.getId()).isEqualTo("id1");
+        assertThat(document.getSchemaType()).isEqualTo("schemaType1");
+        assertThat(document.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
+        assertThat(document.getPropertyDoubleArray("doubleKey1")).usingExactEquality()
+                .containsExactly(1.0, 2.0, 3.0).inOrder();
+        assertThat(document.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true).inOrder();
+        assertThat(document.getPropertyStringArray("stringKey1")).asList()
+                .containsExactly("test-value1", "test-value2", "test-value3").inOrder();
+        assertThat(document.getPropertyEmbeddingArray("embeddingKey1")).asList()
+                .containsExactly(sEmbedding1, sEmbedding2).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocument_setEmptyEmbeddingValues() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setPropertyBoolean("booleanKey")
+                .setPropertyString("stringKey")
+                .setPropertyBytes("byteKey")
+                .setPropertyDouble("doubleKey")
+                .setPropertyDocument("documentKey")
+                .setPropertyLong("longKey")
+                .setPropertyEmbedding("embeddingKey")
+                .build();
+        assertThat(document.getPropertyBooleanArray("booleanKey")).isEmpty();
+        assertThat(document.getPropertyStringArray("stringKey")).isEmpty();
+        assertThat(document.getPropertyBytesArray("byteKey")).isEmpty();
+        assertThat(document.getPropertyDoubleArray("doubleKey")).isEmpty();
+        assertThat(document.getPropertyDocumentArray("documentKey")).isEmpty();
+        assertThat(document.getPropertyLongArray("longKey")).isEmpty();
+        assertThat(document.getPropertyEmbeddingArray("embeddingKey")).isEmpty();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentInvalid_setNullEmbeddingValues() {
+        GenericDocument.Builder builder = new GenericDocument.Builder<>("namespace", "id1",
+                "schemaType1");
+        EmbeddingVector nullEmbedding = null;
+
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setPropertyEmbedding("propEmbeddings",
+                        new EmbeddingVector[]{sEmbedding1, nullEmbedding}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocument_toBuilderWithEmbeddingValues() {
+        GenericDocument document1 = new GenericDocument.Builder<>(
+                /*namespace=*/"", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyBoolean("booleanKey1", true, false, true)
+                .setPropertyString("stringKey1", "String1", "String2", "String3")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1, sEmbedding2)
+                .build();
+        GenericDocument document2 =
+                new GenericDocument.Builder<>(document1)
+                        .setId("id2")
+                        .setNamespace("namespace2")
+                        .setPropertyEmbedding("embeddingKey1", sEmbedding2)
+                        .setPropertyLong("longKey2", 10L)
+                        .clearProperty("booleanKey1")
+                        .build();
+
+        // Make sure old doc hasn't changed
+        assertThat(document1.getId()).isEqualTo("id1");
+        assertThat(document1.getNamespace()).isEqualTo("");
+        assertThat(document1.getPropertyLongArray("longKey1")).asList()
+                .containsExactly(1L, 2L, 3L).inOrder();
+        assertThat(document1.getPropertyBooleanArray("booleanKey1")).asList()
+                .containsExactly(true, false, true).inOrder();
+        assertThat(document1.getPropertyLongArray("longKey2")).isNull();
+        assertThat(document1.getPropertyEmbeddingArray("embeddingKey1")).asList()
+                .containsExactly(sEmbedding1, sEmbedding2).inOrder();
+
+        // Make sure the new doc contains the expected values
+        GenericDocument expectedDoc = new GenericDocument.Builder<>(
+                "namespace2", "id2", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setPropertyLong("longKey1", 1L, 2L, 3L)
+                .setPropertyLong("longKey2", 10L)
+                .setPropertyDouble("doubleKey1", 1.0, 2.0, 3.0)
+                .setPropertyString("stringKey1", "String1", "String2", "String3")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding2)
+                .build();
+        assertThat(document2).isEqualTo(expectedDoc);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testDocumentGetPropertyNamesWithEmbeddingValue() {
+        GenericDocument document = new GenericDocument.Builder<>("namespace", "id1", "schemaType1")
+                .setCreationTimestampMillis(5L)
+                .setScore(1)
+                .setTtlMillis(1L)
+                .setPropertyLong("longKey1", 1L)
+                .setPropertyDouble("doubleKey1", 1.0)
+                .setPropertyBoolean("booleanKey1", true)
+                .setPropertyString("stringKey1", "test-value1")
+                .setPropertyEmbedding("embeddingKey1", sEmbedding1)
+                .build();
+        assertThat(document.getPropertyNames()).containsExactly("longKey1", "doubleKey1",
+                "booleanKey1", "stringKey1", "embeddingKey1");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingValuesCannotBeEmpty() {
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new EmbeddingVector(new float[]{}, "my_model"));
+        assertThat(exception).hasMessageThat().contains("Embedding values cannot be empty.");
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
index 405db5a..4d5a4f9 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GetSchemaResponseCtsTest.java
@@ -23,6 +23,7 @@
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.GetSchemaResponse;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 
 import com.google.common.collect.ImmutableSet;
@@ -30,6 +31,7 @@
 import org.junit.Test;
 
 import java.util.Arrays;
+import java.util.Map;
 
 public class GetSchemaResponseCtsTest {
     @Test
@@ -76,10 +78,10 @@
                         ImmutableSet.of(packageIdentifier2))
                 .setRequiredPermissionsForSchemaTypeVisibility("Email2",
                         ImmutableSet.of(
-                                        ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
-                                                SetSchemaRequest.READ_EXTERNAL_STORAGE),
-                                        ImmutableSet.of(SetSchemaRequest
-                                                .READ_ASSISTANT_APP_SEARCH_DATA))
+                                ImmutableSet.of(SetSchemaRequest.READ_CONTACTS,
+                                        SetSchemaRequest.READ_EXTERNAL_STORAGE),
+                                ImmutableSet.of(SetSchemaRequest
+                                        .READ_ASSISTANT_APP_SEARCH_DATA))
                 ).build();
 
         // rebuild won't effect the original object
@@ -156,6 +158,30 @@
                                 .READ_ASSISTANT_APP_SEARCH_DATA)));
     }
 
+
+    @Test
+    public void setVisibilityConfig() {
+        SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+        SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg3", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg4", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(3, 4))
+                .build();
+
+        GetSchemaResponse getSchemaResponse =
+                new GetSchemaResponse.Builder().setVersion(42)
+                        .setSchemaTypeVisibleToConfigs("Email",
+                                ImmutableSet.of(visibilityConfig1, visibilityConfig2))
+                        .build();
+
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()).containsExactly("Email",
+                ImmutableSet.of(visibilityConfig1, visibilityConfig2));
+    }
+
     @Test
     public void getEmptyVisibility() {
         GetSchemaResponse getSchemaResponse =
@@ -166,11 +192,19 @@
         assertThat(getSchemaResponse.getRequiredPermissionsForSchemaTypeVisibility()).isEmpty();
     }
 
+    @Test
+    public void getEmptyVisibility_visibilityConfig() {
+        GetSchemaResponse getSchemaResponse =
+                new GetSchemaResponse.Builder().setVersion(42)
+                        .build();
+        assertThat(getSchemaResponse.getSchemaTypesVisibleToConfigs()).isEmpty();
+    }
+
     // @exportToFramework:startStrip()
     // Not exported as setVisibilitySettingSupported is hidden in framework
-     /**
+    /**
      * Makes sure an exception is thrown when visibility getters are called after visibility is set
-      * to no supported.
+     * to no supported.
      */
     @Test
     public void setVisibility_setFalse() {
@@ -209,6 +243,14 @@
                 getSchemaResponse::getRequiredPermissionsForSchemaTypeVisibility);
         assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
                 + " this backend/Android API level combination.");
+        e = assertThrows(UnsupportedOperationException.class,
+                getSchemaResponse::getPubliclyVisibleSchemas);
+        assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+                + " this backend/Android API level combination.");
+        e = assertThrows(UnsupportedOperationException.class,
+                getSchemaResponse::getSchemaTypesVisibleToConfigs);
+        assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+                + " this backend/Android API level combination.");
     }
 
     /**
@@ -248,4 +290,56 @@
                 original::getRequiredPermissionsForSchemaTypeVisibility);
     }
     // @exportToFramework:endStrip()
+
+    @Test
+    public void testVisibility_publicVisibility() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 1);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+
+        GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+                .setPubliclyVisibleSchema("Email1", packageIdentifier2)
+                .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+                .build();
+        assertThat(getSchemaResponse.getPubliclyVisibleSchemas().get("Email1"))
+                .isEqualTo(packageIdentifier1);
+    }
+
+    // @exportToFramework:startStrip()
+    // Not exported as setVisibilitySettingSupported is hidden in framework
+    @Test
+    public void testVisibility_publicVisibility_clearVisibility() {
+        byte[] sha256cert1 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+                .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+                // This should clear all visibility settings.
+                .setVisibilitySettingSupported(true)
+                .build();
+
+        Map publiclyVisibleSchemas =
+                getSchemaResponse.getPubliclyVisibleSchemas();
+        assertThat(publiclyVisibleSchemas).isEmpty();
+    }
+
+    @Test
+    public void testVisibility_publicVisibility_notSupported() {
+        byte[] sha256cert1 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        GetSchemaResponse getSchemaResponse = new GetSchemaResponse.Builder()
+                .setPubliclyVisibleSchema("Email1", packageIdentifier1)
+                .setVisibilitySettingSupported(false)
+                .build();
+
+        Exception e = assertThrows(UnsupportedOperationException.class,
+                getSchemaResponse::getPubliclyVisibleSchemas);
+        assertThat(e.getMessage()).isEqualTo("Get visibility setting is not supported with"
+                + " this backend/Android API level combination.");
+    }
+    // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
index 9f38b9c..7df269c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
@@ -751,7 +751,7 @@
                 snapshotResults("body", new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                                SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                         .build());
         assertThat(documents).containsExactly(inEmail4);
 
@@ -761,7 +761,7 @@
                 snapshotResults("body", new SearchSpec.Builder()
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultGrouping(
-                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*resultLimit=*/ 1)
+                                SearchSpec.GROUPING_TYPE_PER_NAMESPACE, /*limit=*/ 1)
                         .build());
         assertThat(documents).containsExactly(inEmail4, inEmail3);
 
@@ -772,7 +772,7 @@
                         .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
                         .setResultGrouping(
                                 SearchSpec.GROUPING_TYPE_PER_NAMESPACE
-                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*resultLimit=*/ 1)
+                                        | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 1)
                         .build());
         assertThat(documents).containsExactly(inEmail4, inEmail3);
     }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
index c2b06d4..a50f7fc 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/PutDocumentsRequestCtsTest.java
@@ -20,14 +20,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.content.Context;
 
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.PutDocumentsRequest;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.appsearch.testutil.AppSearchEmail;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.SearchAction;
+import androidx.appsearch.usagereporting.TakenAction;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableSet;
@@ -50,7 +56,99 @@
         assertThat(request.getGenericDocuments().get(1).getId()).isEqualTo("test2");
     }
 
-// @exportToFramework:startStrip()
+    @Test
+    public void duplicateIdForNormalAndTakenActionGenericDocumentThrowsException()
+            throws Exception {
+        GenericDocument normalDocument = new GenericDocument.Builder<>(
+                "namespace", "id", "builtin:Thing").build();
+        GenericDocument takenActionGenericDocument = new GenericDocument.Builder<>(
+                "namespace", "id", "builtin:ClickAction").build();
+
+        PutDocumentsRequest.Builder builder = new PutDocumentsRequest.Builder()
+                .addGenericDocuments(normalDocument)
+                .addTakenActionGenericDocuments(takenActionGenericDocument);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> builder.build());
+        assertThat(e.getMessage()).isEqualTo("Document id " + takenActionGenericDocument.getId()
+                + " cannot exist in both taken action and normal document");
+    }
+
+    @Test
+    public void addTakenActionGenericDocuments() throws Exception {
+        GenericDocument searchActionGenericDocument1 = new GenericDocument.Builder<>(
+                "namespace", "search1", "builtin:SearchAction").build();
+        GenericDocument clickActionGenericDocument1 = new GenericDocument.Builder<>(
+                "namespace", "click1", "builtin:ClickAction").build();
+        GenericDocument clickActionGenericDocument2 = new GenericDocument.Builder<>(
+                "namespace", "click2", "builtin:ClickAction").build();
+        GenericDocument searchActionGenericDocument2 = new GenericDocument.Builder<>(
+                "namespace", "search2", "builtin:SearchAction").build();
+        GenericDocument clickActionGenericDocument3 = new GenericDocument.Builder<>(
+                "namespace", "click3", "builtin:ClickAction").build();
+        GenericDocument clickActionGenericDocument4 = new GenericDocument.Builder<>(
+                "namespace", "click4", "builtin:ClickAction").build();
+        GenericDocument clickActionGenericDocument5 = new GenericDocument.Builder<>(
+                "namespace", "click5", "builtin:ClickAction").build();
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActionGenericDocuments(
+                        searchActionGenericDocument1, clickActionGenericDocument1,
+                        clickActionGenericDocument2, searchActionGenericDocument2,
+                        clickActionGenericDocument3, clickActionGenericDocument4,
+                        clickActionGenericDocument5)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
+
+    @Test
+    public void addTakenActionGenericDocuments_byCollection() throws Exception {
+        Set takenActionGenericDocuments = ImmutableSet.of(
+                new GenericDocument.Builder<>(
+                        "namespace", "search1", "builtin:SearchAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click1", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click2", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "search2", "builtin:SearchAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click3", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click4", "builtin:ClickAction").build(),
+                new GenericDocument.Builder<>(
+                        "namespace", "click5", "builtin:ClickAction").build());
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActionGenericDocuments(takenActionGenericDocuments)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
+
+    // @exportToFramework:startStrip()
     @Document
     static class Card {
         @Document.Namespace
@@ -87,5 +185,84 @@
 
         assertThat(request.getGenericDocuments().get(0).getId()).isEqualTo("cardId");
     }
+
+    @Test
+    public void addTakenActions() throws Exception {
+        SearchAction searchAction1 =
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .build();
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .build();
+        ClickAction clickAction2 =
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .build();
+        SearchAction searchAction2 =
+                new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */4000)
+                        .build();
+        ClickAction clickAction3 =
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */5000)
+                        .build();
+        ClickAction clickAction4 =
+                new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */6000)
+                        .build();
+        ClickAction clickAction5 =
+                new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */7000)
+                        .build();
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActions(searchAction1, clickAction1, clickAction2, searchAction2,
+                        clickAction3, clickAction4, clickAction5)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
+
+    @Test
+    public void addTakenActions_byCollection() throws Exception {
+        Set takenActions = ImmutableSet.of(
+                new SearchAction.Builder("namespace", "search1", /* actionTimestampMillis= */1000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click1", /* actionTimestampMillis= */2000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click2", /* actionTimestampMillis= */3000)
+                        .build(),
+                new SearchAction.Builder("namespace", "search2", /* actionTimestampMillis= */4000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click3", /* actionTimestampMillis= */5000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click4", /* actionTimestampMillis= */6000)
+                        .build(),
+                new ClickAction.Builder("namespace", "click5", /* actionTimestampMillis= */7000)
+                        .build());
+
+        PutDocumentsRequest request = new PutDocumentsRequest.Builder()
+                .addTakenActions(takenActions)
+                .build();
+
+        // Generic documents should contain nothing.
+        assertThat(request.getGenericDocuments()).isEmpty();
+
+        // Taken action generic documents should contain correct taken action generic documents.
+        assertThat(request.getTakenActionGenericDocuments()).hasSize(7);
+        assertThat(request.getTakenActionGenericDocuments().get(0).getId()).isEqualTo("search1");
+        assertThat(request.getTakenActionGenericDocuments().get(1).getId()).isEqualTo("click1");
+        assertThat(request.getTakenActionGenericDocuments().get(2).getId()).isEqualTo("click2");
+        assertThat(request.getTakenActionGenericDocuments().get(3).getId()).isEqualTo("search2");
+        assertThat(request.getTakenActionGenericDocuments().get(4).getId()).isEqualTo("click3");
+        assertThat(request.getTakenActionGenericDocuments().get(5).getId()).isEqualTo("click4");
+        assertThat(request.getTakenActionGenericDocuments().get(6).getId()).isEqualTo("click5");
+    }
 // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java
new file mode 100644
index 0000000..6787a56f
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SchemaVisibilityConfigCtsTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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 androidx.appsearch.cts.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class SchemaVisibilityConfigCtsTest {
+
+    @Test
+    public void testBuildVisibilityConfig() {
+        byte[] cert1 = new byte[32];
+        Arrays.fill(cert1, (byte) 1);
+        byte[] cert2 = new byte[32];
+        Arrays.fill(cert2, (byte) 2);
+        SchemaVisibilityConfig schemaVisibilityConfig = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", cert1))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", cert2))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        assertThat(schemaVisibilityConfig.getRequiredPermissions())
+                .containsExactly(ImmutableSet.of(1, 2));
+        assertThat(schemaVisibilityConfig.getAllowedPackages())
+                .containsExactly(new PackageIdentifier("pkg1", cert1));
+        assertThat(schemaVisibilityConfig.getPubliclyVisibleTargetPackage())
+                .isEqualTo(new PackageIdentifier("pkg2", cert2));
+    }
+
+    @Test
+    public void testVisibilityConfigEquals() {
+        // Create two VisibilityConfig instances with the same properties
+        SchemaVisibilityConfig visibilityConfig1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        SchemaVisibilityConfig visibilityConfig2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier("pkg1", new byte[32]))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier("pkg2", new byte[32]))
+                .addRequiredPermissions(ImmutableSet.of(1, 2))
+                .build();
+
+        // Test equals method
+        assertThat(visibilityConfig1).isEqualTo(visibilityConfig2);
+        assertThat(visibilityConfig2).isEqualTo(visibilityConfig1);
+    }
+
+    @Test
+    public void testVisibilityConfig_rebuild() {
+        String visibleToPackage = "com.example.package";
+        byte[] visibleToPackageCert = new byte[32];
+
+        String publiclyVisibleTarget = "com.example.test";
+        byte[] publiclyVisibleTargetCert = new byte[32];
+
+        SchemaVisibilityConfig.Builder builder = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(new PackageIdentifier(visibleToPackage, visibleToPackageCert))
+                .setPubliclyVisibleTargetPackage(new PackageIdentifier(
+                        publiclyVisibleTarget, publiclyVisibleTargetCert))
+                .addRequiredPermissions(ImmutableSet.of(1, 2));
+
+        // Create a VisibilityConfig using the Builder
+        SchemaVisibilityConfig original = builder.build();
+
+        SchemaVisibilityConfig rebuild = builder.clearAllowedPackages()
+                .setPubliclyVisibleTargetPackage(null)
+                .clearRequiredPermissions().build();
+
+        // Check if the properties are set correctly
+        assertThat(original.getAllowedPackages()).containsExactly(
+                new PackageIdentifier(visibleToPackage, visibleToPackageCert));
+        assertThat(original.getPubliclyVisibleTargetPackage()).isEqualTo(
+                new PackageIdentifier(publiclyVisibleTarget, publiclyVisibleTargetCert));
+        assertThat(original.getRequiredPermissions()).containsExactly(ImmutableSet.of(1, 2));
+
+        assertThat(rebuild.getAllowedPackages()).isEmpty();
+        assertThat(rebuild.getPubliclyVisibleTargetPackage()).isNull();
+        assertThat(rebuild.getRequiredPermissions()).isEmpty();
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
index 426d01a..382d5272 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchResultCtsTest.java
@@ -22,11 +22,18 @@
 
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 import androidx.appsearch.testutil.AppSearchEmail;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 public class SearchResultCtsTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Test
     public void testBuildSearchResult() {
@@ -172,4 +179,48 @@
         SearchResult rebuildJoinedResult2 = rebuild.getJoinedResults().get(1);
         assertThat(rebuildJoinedResult2.getGenericDocument().getId()).isEqualTo("id3");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testBuildSearchResult_informationalRankingSignals() {
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+        SearchResult searchResult = new SearchResult.Builder("packageName", "databaseName")
+                .setGenericDocument(email)
+                .setRankingSignal(2.9)
+                .addInformationalRankingSignal(3.0)
+                .addInformationalRankingSignal(4.0)
+                .build();
+
+        assertThat(searchResult.getRankingSignal()).isEqualTo(2.9);
+        assertThat(searchResult.getInformationalRankingSignals())
+                .containsExactly(3.0, 4.0).inOrder();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testRebuild_informationalRankingSignals() {
+        AppSearchEmail email = new AppSearchEmail.Builder("namespace1", "id1")
+                .setBody("Hello World.")
+                .build();
+
+        SearchResult.Builder searchResultBuilder =
+                new SearchResult.Builder("packageName", "databaseName")
+                        .setGenericDocument(email)
+                        .setRankingSignal(2.9)
+                        .addInformationalRankingSignal(3.0)
+                        .addInformationalRankingSignal(4.0);
+
+        SearchResult original = searchResultBuilder.build();
+        SearchResult rebuild = searchResultBuilder.addInformationalRankingSignal(5).build();
+
+        // Rebuild won't effect the original object
+        assertThat(original.getRankingSignal()).isEqualTo(2.9);
+        assertThat(original.getInformationalRankingSignals()).containsExactly(3.0, 4.0).inOrder();
+
+        assertThat(rebuild.getRankingSignal()).isEqualTo(2.9);
+        assertThat(rebuild.getInformationalRankingSignals())
+                .containsExactly(3.0, 4.0, 5.0).inOrder();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index f465eaf..a6820fb 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -24,14 +24,20 @@
 import static org.junit.Assert.assertThrows;
 
 import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.JoinSpec;
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.flags.CheckFlagsRule;
+import androidx.appsearch.flags.DeviceFlagsValueProvider;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.flags.RequiresFlagsEnabled;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Collections;
@@ -40,6 +46,9 @@
 import java.util.Set;
 
 public class SearchSpecCtsTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void testBuildSearchSpecWithoutTermMatch() {
         SearchSpec searchSpec = new SearchSpec.Builder().addFilterSchemas("testSchemaType").build();
@@ -118,6 +127,52 @@
     }
 
     @Test
+    public void testBuildSearchSpec_searchSourceLogTag() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setSearchSourceLogTag("logTag")
+                .build();
+
+        assertThat(searchSpec.getSearchSourceLogTag()).isEqualTo("logTag");
+    }
+
+    @Test
+    public void testBuildSearchSpec_searchSourceLogTag_exceedLengthLimitation() {
+        String longTag = new String(new char[110]);
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new SearchSpec.Builder()
+                        .setSearchSourceLogTag(longTag));
+        assertThat(e).hasMessageThat().contains(
+                "The maximum supported tag length is 100. This tag is too long");
+    }
+
+    @Test
+    public void testBuildSearchSpec_searchSourceLogTag_defaultIsNull() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .build();
+
+        assertThat(searchSpec.getSearchSourceLogTag()).isNull();
+    }
+
+    // TODO(b/309826655): Flag guard this test.
+    @Test
+    public void testBuildSearchSpec_hasProperty() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+
+        assertThat(searchSpec.isNumericSearchEnabled()).isTrue();
+        assertThat(searchSpec.isVerbatimSearchEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+    }
+
+    @Test
     public void testGetProjectionTypePropertyMasks() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -404,6 +459,24 @@
         assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
     }
 
+    // TODO(b/309826655): Flag guard this test.
+    @Test
+    public void testSetFeatureEnabledToFalse_hasProperty() {
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+
+        searchSpec = builder
+                .setListFilterQueryLanguageEnabled(false)
+                .setListFilterHasPropertyFunctionEnabled(false)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isFalse();
+    }
 
     @Test
     public void testInvalidAdvancedRanking() {
@@ -463,20 +536,6 @@
     }
 
     @Test
-    public void testProjections_withSchemaFilter() throws Exception {
-        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
-                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addFilterSchemas("Filter")
-                .addProjectionPathsForDocumentClass(King.class, ImmutableList.of(
-                        new PropertyPath("field1"), new PropertyPath("field2.subfield2")));
-
-        IllegalArgumentException exception =
-                assertThrows(IllegalArgumentException.class, searchSpecBuilder::build);
-        assertThat(exception.getMessage())
-                .isEqualTo("Projection requested for schema not in schemas filters: King");
-    }
-
-    @Test
     public void testTypePropertyWeightsForDocumentClass() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
@@ -553,6 +612,40 @@
     }
 
     @Test
+    public void testGetPropertyFiltersTypePropertyMasks() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addFilterProperties("TypeA", ImmutableList.of("field1", "field2.subfield2"))
+                .addFilterProperties("TypeB", ImmutableList.of("field7"))
+                .addFilterProperties("TypeC", ImmutableList.of())
+                .build();
+
+        Map> typePropertyPathMap = searchSpec.getFilterProperties();
+        assertThat(typePropertyPathMap.keySet())
+                .containsExactly("TypeA", "TypeB", "TypeC");
+        assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
+        assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
+        assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
+    }
+
+    @Test
+    public void testFilterSchemas_wildcardProjection() {
+        // Should not crash
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("ParentType")
+                .addProjection(SearchSpec.SCHEMA_TYPE_WILDCARD, Collections.singletonList("TypeA"))
+                .addFilterProperties(SearchSpec.SCHEMA_TYPE_WILDCARD,
+                        Collections.singletonList("TypeB"))
+                .build();
+
+        assertThat(searchSpec.getFilterSchemas()).containsExactly("ParentType");
+        assertThat(searchSpec.getProjections())
+                .containsExactly(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("TypeA"));
+        assertThat(searchSpec.getFilterProperties())
+                .containsExactly(SearchSpec.SCHEMA_TYPE_WILDCARD, ImmutableList.of("TypeB"));
+    }
+
+    @Test
     public void testRebuild() {
         JoinSpec originalJoinSpec = new JoinSpec.Builder("entityId")
                 .setNestedSearch("joe", new SearchSpec.Builder().addFilterSchemas("Action").build())
@@ -583,4 +676,163 @@
         assertThat(rebuild.getJoinSpec().getNestedSearchSpec().getFilterSchemas())
                 .containsExactly("CallAction");
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testEmbeddingSearch() {
+        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();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+        assertThat(searchSpec.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+        assertThat(searchSpec.getSearchEmbeddings()).containsExactly(embedding1, embedding2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testRebuild_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");
+
+        // Create a builder
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .setDefaultEmbeddingSearchMetricType(
+                        SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT)
+                .addSearchEmbeddings(embedding1);
+        SearchSpec searchSpec1 = searchSpecBuilder.build();
+
+        // Add a new embedding to the builder and rebuild. We should see that the new embedding
+        // is only added to searchSpec2.
+        searchSpecBuilder.addSearchEmbeddings(embedding2);
+        SearchSpec searchSpec2 = searchSpecBuilder.build();
+
+        assertThat(searchSpec1.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec1.isEmbeddingSearchEnabled()).isTrue();
+        assertThat(searchSpec1.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+        assertThat(searchSpec1.getSearchEmbeddings()).containsExactly(embedding1);
+
+        assertThat(searchSpec2.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec2.isEmbeddingSearchEnabled()).isTrue();
+        assertThat(searchSpec2.getDefaultEmbeddingSearchMetricType()).isEqualTo(
+                SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_DOT_PRODUCT);
+        assertThat(searchSpec2.getSearchEmbeddings()).containsExactly(embedding1, embedding2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testBuildSearchSpec_embeddingSearch() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setNumericSearchEnabled(true)
+                .setVerbatimSearchEnabled(true)
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterHasPropertyFunctionEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+
+        assertThat(searchSpec.isNumericSearchEnabled()).isTrue();
+        assertThat(searchSpec.isVerbatimSearchEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterHasPropertyFunctionEnabled()).isTrue();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
+    public void testSetFeatureEnabledToFalse_embeddingSearch() {
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder
+                .setListFilterQueryLanguageEnabled(true)
+                .setEmbeddingSearchEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isTrue();
+
+        searchSpec = builder
+                .setListFilterQueryLanguageEnabled(false)
+                .setEmbeddingSearchEnabled(false)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isFalse();
+        assertThat(searchSpec.isEmbeddingSearchEnabled()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testListFilterTokenizeFunction() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION)
+    public void testSetFeatureEnabledToFalse_tokenizeFunction() {
+        SearchSpec.Builder builder = new SearchSpec.Builder();
+        SearchSpec searchSpec = builder
+                .setListFilterQueryLanguageEnabled(true)
+                .setListFilterTokenizeFunctionEnabled(true)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isTrue();
+
+        searchSpec = builder
+                .setListFilterTokenizeFunctionEnabled(false)
+                .build();
+        assertThat(searchSpec.isListFilterQueryLanguageEnabled()).isTrue();
+        assertThat(searchSpec.isListFilterTokenizeFunctionEnabled()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testInformationalRankingExpressions() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setOrder(SearchSpec.ORDER_ASCENDING)
+                .setRankingStrategy("this.documentScore()")
+                .addInformationalRankingExpressions("this.relevanceScore()")
+                .build();
+        assertThat(searchSpec.getOrder()).isEqualTo(SearchSpec.ORDER_ASCENDING);
+        assertThat(searchSpec.getRankingStrategy())
+                .isEqualTo(SearchSpec.RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION);
+        assertThat(searchSpec.getAdvancedRankingExpression())
+                .isEqualTo("this.documentScore()");
+        assertThat(searchSpec.getInformationalRankingExpressions()).containsExactly(
+                "this.relevanceScore()");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS)
+    public void testRebuild_informationalRankingExpressions() {
+        SearchSpec.Builder searchSpecBuilder =
+                new SearchSpec.Builder().addInformationalRankingExpressions(
+                        "this.relevanceScore()");
+
+        SearchSpec original = searchSpecBuilder.build();
+        SearchSpec rebuild = searchSpecBuilder
+                .addInformationalRankingExpressions("this.documentScore()")
+                .build();
+
+        // Rebuild won't effect the original object
+        assertThat(original.getInformationalRankingExpressions())
+                .containsExactly("this.relevanceScore()");
+
+        assertThat(rebuild.getInformationalRankingExpressions())
+                .containsExactly("this.relevanceScore()", "this.documentScore()").inOrder();
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
index 69979c3..6f1e79b 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSuggestionSpecCtsTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchSuggestionSpec;
 
 import com.google.common.collect.ImmutableList;
@@ -75,6 +76,38 @@
     }
 
     @Test
+    public void testBuildSearchSuggestionSpec_withPropertyFilter() throws Exception {
+        SearchSuggestionSpec searchSuggestionSpec =
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+                        .setRankingStrategy(SearchSuggestionSpec
+                                .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
+                        .addFilterSchemas("Person", "Email")
+                        .addFilterSchemas(ImmutableList.of("Foo"))
+                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
+                        .addFilterPropertyPaths("Foo",
+                                ImmutableList.of(new PropertyPath("Bar")))
+                        .build();
+
+        assertThat(searchSuggestionSpec.getMaximumResultCount()).isEqualTo(123);
+        assertThat(searchSuggestionSpec.getFilterSchemas())
+                .containsExactly("Person", "Email", "Foo");
+        assertThat(searchSuggestionSpec.getFilterProperties())
+                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
+                        "Foo",  ImmutableList.of("Bar"));
+    }
+
+    @Test
+    public void testPropertyFilterMustMatchSchemaFilter() throws Exception {
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+                        .addFilterSchemas("Person")
+                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"))
+                        .build());
+        assertThat(e).hasMessageThat().contains("The schema: Email exists in the "
+                + "property filter but doesn't exist in the schema filter.");
+    }
+
+    @Test
     public void testRebuild() throws Exception {
         SearchSuggestionSpec.Builder builder =
                 new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
@@ -106,4 +139,31 @@
         assertThat(rebuild.getFilterSchemas())
                 .containsExactly("Person", "Email", "Message", "Foo");
     }
+
+    @Test
+    public void testRebuild_withPropertyFilter() throws Exception {
+        SearchSuggestionSpec.Builder builder =
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/123)
+                        .addFilterSchemas("Person", "Email")
+                        .addFilterProperties("Email", ImmutableList.of("Subject", "body"));
+
+        SearchSuggestionSpec original = builder.build();
+
+        builder.addFilterSchemas("Message", "Foo")
+                .addFilterProperties("Foo", ImmutableList.of("Bar"));
+        SearchSuggestionSpec rebuild = builder.build();
+
+        assertThat(original.getMaximumResultCount()).isEqualTo(123);
+        assertThat(original.getFilterSchemas())
+                .containsExactly("Person", "Email");
+        assertThat(original.getFilterProperties())
+                .containsExactly("Email",  ImmutableList.of("Subject", "body"));
+
+        assertThat(rebuild.getMaximumResultCount()).isEqualTo(123);
+        assertThat(rebuild.getFilterSchemas())
+                .containsExactly("Person", "Email", "Message", "Foo");
+        assertThat(rebuild.getFilterProperties())
+                .containsExactly("Email",  ImmutableList.of("Subject", "body"),
+                        "Foo",  ImmutableList.of("Bar"));
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
index d7de450..1876b87 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -30,6 +30,7 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SchemaVisibilityConfig;
 import androidx.appsearch.app.SetSchemaRequest;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.testutil.AppSearchEmail;
@@ -201,6 +202,32 @@
     }
 
     @Test
+    public void testInvalidSchemaReferences_fromPubliclyVisible() {
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setPubliclyVisibleSchema("InvalidSchema",
+                        new PackageIdentifier("com.foo.package",
+                                /*sha256Certificate=*/ new byte[]{})).build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
+    public void testInvalidSchemaReferences_fromVisibleToConfigs() {
+        byte[] sha256cert1 = new byte[32];
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        SchemaVisibilityConfig config = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder()
+                        .addSchemaTypeVisibleToConfig("InvalidSchema", config)
+                        .build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
     public void testSetSchemaTypeDisplayedBySystem_displayed() {
         AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
 
@@ -276,7 +303,7 @@
                         "Schema2", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Clear the permissions in the builder
         setSchemaRequestBuilder.clearRequiredPermissionsForSchemaTypeVisibility("Schema1");
@@ -287,7 +314,7 @@
                         "Schema2", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Old object should remain unchanged
         assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -300,7 +327,7 @@
                         "Schema2", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
     }
 
     @Test
@@ -375,6 +402,132 @@
         assertThat(request.getSchemasVisibleToPackages()).isEmpty();
     }
 
+    @Test
+    public void testPubliclyVisibleSchemaType() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+                        "Schema", packageIdentifier).build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+    }
+
+    @Test
+    public void testPubliclyVisibleSchemaType_removal() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+                        "Schema", packageIdentifier).build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+
+        // Removed Schema
+        request = new SetSchemaRequest.Builder().addSchemas(schema)
+                .setPubliclyVisibleSchema("Schema", packageIdentifier)
+                .setPubliclyVisibleSchema("Schema", null)
+                .build();
+        assertThat(request.getPubliclyVisibleSchemas()).isEmpty();
+    }
+
+    @Test
+    public void testPubliclyVisibleSchemaType_deduped() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier =
+                new PackageIdentifier("com.package.foo", /*sha256Certificate=*/ new byte[]{});
+        PackageIdentifier packageIdentifier2 =
+                new PackageIdentifier("com.package.bar", /*sha256Certificate=*/ new byte[]{});
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchemas(schema).setPubliclyVisibleSchema(
+                        "Schema", packageIdentifier).build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+
+        // Deduped schema
+        request = new SetSchemaRequest.Builder().addSchemas(schema)
+                .setPubliclyVisibleSchema("Schema", packageIdentifier2)
+                .setPubliclyVisibleSchema("Schema", packageIdentifier)
+                .build();
+        assertThat(request.getPubliclyVisibleSchemas())
+                .containsExactly("Schema", packageIdentifier);
+    }
+
+    @Test
+    public void testSetSchemaTypeVisibleForConfigs() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .addSchemaTypeVisibleToConfig("Schema", config1)
+                .addSchemaTypeVisibleToConfig("Schema", config2)
+                .build();
+
+        assertThat(request.getSchemasVisibleToConfigs()).containsExactly("Schema",
+                ImmutableSet.of(config1, config2));
+    }
+
+    @Test
+    public void testClearSchemaTypeVisibleForConfigs() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addSchemas(schema)
+                .addSchemaTypeVisibleToConfig("Schema", config1)
+                .addSchemaTypeVisibleToConfig("Schema", config2);
+
+        SetSchemaRequest original = builder.build();
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("Schema",
+                ImmutableSet.of(config1, config2));
+
+        builder.clearSchemaTypeVisibleToConfigs("Schema");
+        SetSchemaRequest rebuild = builder.build();
+
+        // rebuild has empty visible to configs
+        assertThat(rebuild.getSchemasVisibleToConfigs()).isEmpty();
+        // original keep in the same state
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("Schema",
+                ImmutableSet.of(config1, config2));
+    }
 
     // @exportToFramework:startStrip()
     @Document
@@ -611,7 +764,7 @@
                         "Queen", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Clear the permissions in the builder
         setSchemaRequestBuilder.clearRequiredPermissionsForDocumentClassVisibility(King.class);
@@ -622,7 +775,7 @@
                         "Queen", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
 
         // Old object should remain unchanged
         assertThat(request.getRequiredPermissionsForSchemaTypeVisibility())
@@ -635,7 +788,76 @@
                         "Queen", ImmutableSet.of(
                                 ImmutableSet.of(SetSchemaRequest.READ_EXTERNAL_STORAGE)
                         )
-            );
+                );
+    }
+
+    @Test
+    public void testSetDocumentClassVisibleForConfigs() throws Exception {
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addDocumentClasses(King.class, Queen.class)
+                .addDocumentClassVisibleToConfig(King.class, config1)
+                .addDocumentClassVisibleToConfig(King.class, config2)
+                .build();
+
+        assertThat(request.getSchemasVisibleToConfigs()).containsExactly("King",
+                ImmutableSet.of(config1, config2));
+    }
+
+    @Test
+    public void testClearDocumentClassVisibleForConfigs() throws Exception {
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("com.package.foo",
+                new byte[]{100});
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("com.package.bar",
+                new byte[]{100});
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addDocumentClasses(King.class, Queen.class)
+                .addDocumentClassVisibleToConfig(King.class, config1)
+                .addDocumentClassVisibleToConfig(King.class, config2);
+
+        SetSchemaRequest original = builder.build();
+
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("King",
+                ImmutableSet.of(config1, config2));
+
+        // Clear the visbleToConfigs
+        builder.clearDocumentClassVisibleToConfigs(King.class);
+        SetSchemaRequest rebuild = builder.build();
+
+        // rebuild object has empty visibleToConfigs
+        assertThat(rebuild.getSchemasVisibleToConfigs()).isEmpty();
+        // original keep in same state.
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly("King",
+                ImmutableSet.of(config1, config2));
     }
 // @exportToFramework:endStrip()
 
@@ -740,6 +962,91 @@
     }
 
     @Test
+    public void testRebuild_visibleConfigs() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+
+        SchemaVisibilityConfig config1 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier1)
+                .addRequiredPermissions(
+                        ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA))
+                .build();
+        SchemaVisibilityConfig config2 = new SchemaVisibilityConfig.Builder()
+                .addAllowedPackage(packageIdentifier2)
+                .addRequiredPermissions(ImmutableSet.of(
+                        SetSchemaRequest.READ_HOME_APP_SEARCH_DATA,
+                        SetSchemaRequest.READ_CALENDAR))
+                .build();
+
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2")
+                .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("subject")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addSchemas(schema1)
+                .addSchemaTypeVisibleToConfig("Email1", config1);
+
+        SetSchemaRequest original = builder.build();
+        SetSchemaRequest rebuild = builder
+                .addSchemas(schema2)
+                .addSchemaTypeVisibleToConfig("Email2", config2)
+                .build();
+
+        assertThat(original.getSchemas()).containsExactly(schema1);
+        assertThat(original.getSchemasVisibleToConfigs()).containsExactly(
+                "Email1", ImmutableSet.of(config1));
+
+        assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+        assertThat(rebuild.getSchemasVisibleToConfigs()).containsExactly(
+                "Email1", ImmutableSet.of(config1),
+                "Email2", ImmutableSet.of(config2));
+    }
+
+    @Test
+    public void testSetVisibility_publicVisibility_rebuild() {
+        byte[] sha256cert1 = new byte[32];
+        byte[] sha256cert2 = new byte[32];
+        Arrays.fill(sha256cert1, (byte) 1);
+        Arrays.fill(sha256cert2, (byte) 2);
+        PackageIdentifier packageIdentifier1 = new PackageIdentifier("Email", sha256cert1);
+        PackageIdentifier packageIdentifier2 = new PackageIdentifier("Email", sha256cert2);
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email1").build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email2").build();
+
+        SetSchemaRequest.Builder builder = new SetSchemaRequest.Builder()
+                .addSchemas(schema1).setPubliclyVisibleSchema("Email1", packageIdentifier1);
+
+        SetSchemaRequest original = builder.build();
+        SetSchemaRequest rebuild = builder.addSchemas(schema2)
+                .setPubliclyVisibleSchema("Email2", packageIdentifier2).build();
+
+        assertThat(original.getSchemas()).containsExactly(schema1);
+        assertThat(original.getPubliclyVisibleSchemas())
+                .containsExactly("Email1", packageIdentifier1);
+
+        assertThat(rebuild.getSchemas()).containsExactly(schema1, schema2);
+        assertThat(original.getPubliclyVisibleSchemas())
+                .containsExactly("Email1", packageIdentifier1);
+    }
+
+    @Test
     public void getAndModify() {
         byte[] sha256cert1 = new byte[32];
         byte[] sha256cert2 = new byte[32];
@@ -769,7 +1076,8 @@
                 .build();
 
         // get the visibility setting and modify the output object.
-        // skip getSchemasNotDisplayedBySystem since it returns an unmodifiable object.
+        // skip getSchemasNotDisplayedBySystem and getPubliclyVisibleSchemas since they return
+        // unmodifiable objects.
         request.getSchemasVisibleToPackages().put("Email2", ImmutableSet.of(packageIdentifier2));
         request.getRequiredPermissionsForSchemaTypeVisibility().put("Email2",
                 ImmutableSet.of(ImmutableSet.of(SetSchemaRequest.READ_CALENDAR)));
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
index 06f1bd4..55840c6 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/observer/DocumentChangeInfoCtsTest.java
@@ -27,17 +27,17 @@
 public class DocumentChangeInfoCtsTest {
     @Test
     public void testConstructor() {
-        DocumentChangeInfo DocumentChangeInfo = new DocumentChangeInfo(
+        DocumentChangeInfo documentChangeInfo = new DocumentChangeInfo(
                 "packageName",
                 "databaseName",
                 "namespace",
                 "SchemaName",
                 ImmutableSet.of("documentId1", "documentId2"));
-        assertThat(DocumentChangeInfo.getPackageName()).isEqualTo("packageName");
-        assertThat(DocumentChangeInfo.getDatabaseName()).isEqualTo("databaseName");
-        assertThat(DocumentChangeInfo.getNamespace()).isEqualTo("namespace");
-        assertThat(DocumentChangeInfo.getSchemaName()).isEqualTo("SchemaName");
-        assertThat(DocumentChangeInfo.getChangedDocumentIds())
+        assertThat(documentChangeInfo.getPackageName()).isEqualTo("packageName");
+        assertThat(documentChangeInfo.getDatabaseName()).isEqualTo("databaseName");
+        assertThat(documentChangeInfo.getNamespace()).isEqualTo("namespace");
+        assertThat(documentChangeInfo.getSchemaName()).isEqualTo("SchemaName");
+        assertThat(documentChangeInfo.getChangedDocumentIds())
                 .containsExactly("documentId1", "documentId2");
     }
 
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java
new file mode 100644
index 0000000..48f7846
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/ClickActionCtsTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.cts.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.ClickAction;
+import androidx.appsearch.usagereporting.TakenAction;
+
+import org.junit.Test;
+
+public class ClickActionCtsTest {
+    @Test
+    public void testBuilder() {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(65536)
+                        .build();
+
+        assertThat(clickAction.getNamespace()).isEqualTo("namespace");
+        assertThat(clickAction.getId()).isEqualTo("id");
+        assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(clickAction.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickAction.getQuery()).isEqualTo("query");
+        assertThat(clickAction.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");
+        assertThat(clickAction.getResultRankInBlock()).isEqualTo(1);
+        assertThat(clickAction.getResultRankGlobal()).isEqualTo(3);
+        assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(65536);
+    }
+
+    @Test
+    public void testBuilder_defaultValues() {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .build();
+
+        assertThat(clickAction.getNamespace()).isEqualTo("namespace");
+        assertThat(clickAction.getId()).isEqualTo("id");
+        assertThat(clickAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(clickAction.getDocumentTtlMillis())
+                .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);
+        assertThat(clickAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickAction.getQuery()).isNull();
+        assertThat(clickAction.getReferencedQualifiedId()).isNull();
+        assertThat(clickAction.getResultRankInBlock()).isEqualTo(-1);
+        assertThat(clickAction.getResultRankGlobal()).isEqualTo(-1);
+        assertThat(clickAction.getTimeStayOnResultMillis()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsAreCopied() {
+        ClickAction clickAction1 =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(65536)
+                        .build();
+        ClickAction clickAction2 = new ClickAction.Builder(clickAction1).build();
+
+        // All fields should be copied correctly from clickAction1 to the builder and propagates to
+        // clickAction2 after calling build().
+        assertThat(clickAction2.getNamespace()).isEqualTo("namespace");
+        assertThat(clickAction2.getId()).isEqualTo("id");
+        assertThat(clickAction2.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(clickAction2.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(clickAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(clickAction2.getQuery()).isEqualTo("query");
+        assertThat(clickAction2.getReferencedQualifiedId()).isEqualTo("pkg$db/ns#refId");
+        assertThat(clickAction2.getResultRankInBlock()).isEqualTo(1);
+        assertThat(clickAction2.getResultRankGlobal()).isEqualTo(3);
+        assertThat(clickAction2.getTimeStayOnResultMillis()).isEqualTo(65536);
+    }
+
+    @Test
+    public void testToGenericDocument() throws Exception {
+        ClickAction clickAction =
+                new ClickAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setReferencedQualifiedId("pkg$db/ns#refId")
+                        .setResultRankInBlock(1)
+                        .setResultRankGlobal(3)
+                        .setTimeStayOnResultMillis(65536)
+                        .build();
+
+        GenericDocument document = GenericDocument.fromDocumentClass(clickAction);
+        assertThat(document.getNamespace()).isEqualTo("namespace");
+        assertThat(document.getId()).isEqualTo("id");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(123);
+        assertThat(document.getTtlMillis()).isEqualTo(456);
+        assertThat(document.getPropertyLong("actionType"))
+                .isEqualTo(ActionConstants.ACTION_TYPE_CLICK);
+        assertThat(document.getPropertyString("query")).isEqualTo("query");
+        assertThat(document.getPropertyString("referencedQualifiedId"))
+                .isEqualTo("pkg$db/ns#refId");
+        assertThat(document.getPropertyLong("resultRankInBlock")).isEqualTo(1);
+        assertThat(document.getPropertyLong("resultRankGlobal")).isEqualTo(3);
+        assertThat(document.getPropertyLong("timeStayOnResultMillis")).isEqualTo(65536);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java
new file mode 100644
index 0000000..a4a63a6
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/usagereporting/SearchActionCtsTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+
+package androidx.appsearch.cts.usagereporting;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.usagereporting.ActionConstants;
+import androidx.appsearch.usagereporting.SearchAction;
+import androidx.appsearch.usagereporting.TakenAction;
+
+import org.junit.Test;
+
+public class SearchActionCtsTest {
+    @Test
+    public void testBuilder() {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                    .setDocumentTtlMillis(456)
+                    .setQuery("query")
+                    .setFetchedResultCount(1)
+                    .build();
+
+        assertThat(searchAction.getNamespace()).isEqualTo("namespace");
+        assertThat(searchAction.getId()).isEqualTo("id");
+        assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(searchAction.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchAction.getQuery()).isEqualTo("query");
+        assertThat(searchAction.getFetchedResultCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testBuilder_defaultValues() {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .build();
+
+        assertThat(searchAction.getNamespace()).isEqualTo("namespace");
+        assertThat(searchAction.getId()).isEqualTo("id");
+        assertThat(searchAction.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(searchAction.getDocumentTtlMillis())
+                .isEqualTo(TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS);
+        assertThat(searchAction.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchAction.getQuery()).isNull();
+        assertThat(searchAction.getFetchedResultCount()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testBuilderCopy_allFieldsAreCopied() {
+        SearchAction searchAction1 =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                        .setDocumentTtlMillis(456)
+                        .setQuery("query")
+                        .setFetchedResultCount(1)
+                        .build();
+        SearchAction searchAction2 = new SearchAction.Builder(searchAction1).build();
+
+        assertThat(searchAction2.getNamespace()).isEqualTo("namespace");
+        assertThat(searchAction2.getId()).isEqualTo("id");
+        assertThat(searchAction2.getActionTimestampMillis()).isEqualTo(123);
+        assertThat(searchAction2.getDocumentTtlMillis()).isEqualTo(456);
+        assertThat(searchAction2.getActionType()).isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(searchAction2.getQuery()).isEqualTo("query");
+        assertThat(searchAction2.getFetchedResultCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testToGenericDocument() throws Exception {
+        SearchAction searchAction =
+                new SearchAction.Builder("namespace", "id", /* actionTimestampMillis= */123)
+                    .setDocumentTtlMillis(456)
+                    .setQuery("query")
+                    .setFetchedResultCount(1)
+                    .build();
+
+        GenericDocument document = GenericDocument.fromDocumentClass(searchAction);
+        assertThat(document.getNamespace()).isEqualTo("namespace");
+        assertThat(document.getId()).isEqualTo("id");
+        assertThat(document.getCreationTimestampMillis()).isEqualTo(123);
+        assertThat(document.getTtlMillis()).isEqualTo(456);
+        assertThat(document.getPropertyLong("actionType"))
+                .isEqualTo(ActionConstants.ACTION_TYPE_SEARCH);
+        assertThat(document.getPropertyString("query")).isEqualTo("query");
+        assertThat(document.getPropertyLong("fetchedResultCount")).isEqualTo(1);
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java
new file mode 100644
index 0000000..3ec5386
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/CheckFlagsRule.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Shim for real CheckFlagsRule defined in Framework.
+ *
+ * 

In Jetpack, this shim does nothing and exists only for code sync purpose. + */ +public final class CheckFlagsRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return base; + } +}

diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java
new file mode 100644
index 0000000..1a2858a
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/DeviceFlagsValueProvider.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+/**
+ * Shim for real DeviceFlagsValueProvider defined in Framework.
+ *
+ * 

In Jetpack, this shim does nothing and exists only for code sync purpose. + */ +public final class DeviceFlagsValueProvider { + private DeviceFlagsValueProvider() {} + + /** Provides a shim rule that can be used to check the status of flags on device */ + public static CheckFlagsRule createCheckFlagsRule() { + return new CheckFlagsRule(); + } +}

diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
new file mode 100644
index 0000000..14169d7
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/FlagsTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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 androidx.appsearch.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class FlagsTest {
+    @Test
+    public void testFlagValue_enableSafeParcelable2() {
+        assertThat(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2).isEqualTo(
+                "com.android.appsearch.flags.enable_safe_parcelable_2");
+    }
+
+    @Test
+    public void testFlagValue_enableListFilterHasPropertyFunction() {
+        assertThat(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION).isEqualTo(
+                "com.android.appsearch.flags.enable_list_filter_has_property_function");
+    }
+
+    @Test
+    public void testFlagValue_enableGroupingTypePerSchema() {
+        assertThat(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA).isEqualTo(
+                "com.android.appsearch.flags.enable_grouping_type_per_schema");
+    }
+
+    @Test
+    public void testFlagValue_enableGenericDocumentCopyConstructor() {
+        assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR).isEqualTo("com.android"
+                + ".appsearch.flags.enable_generic_document_copy_constructor");
+    }
+
+    @Test
+    public void testFlagValue_enableSearchSpecFilterProperties() {
+        assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES).isEqualTo(
+                "com.android.appsearch.flags.enable_search_spec_filter_properties");
+    }
+
+    @Test
+    public void testFlagValue_enableSearchSpecSetSearchSourceLogTag() {
+        assertThat(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG).isEqualTo(
+                "com.android.appsearch.flags.enable_search_spec_set_search_source_log_tag");
+    }
+
+    @Test
+    public void testFlagValue_enableSetSchemaVisibleToConfigs() {
+        assertThat(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS).isEqualTo("com"
+                + ".android.appsearch.flags.enable_set_schema_visible_to_configs");
+    }
+
+    @Test
+    public void testFlagValue_enablePutDocumentsRequestAddTakenActions() {
+        assertThat(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS).isEqualTo(
+                "com.android.appsearch.flags.enable_put_documents_request_add_taken_actions");
+    }
+
+    @Test
+    public void testFlagValue_enableGenericDocumentBuilderHiddenMethods() {
+        assertThat(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS).isEqualTo("com"
+                + ".android.appsearch.flags.enable_generic_document_builder_hidden_methods");
+    }
+
+    @Test
+    public void testFlagValue_enableSetPubliclyVisibleSchema() {
+        assertThat(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
+                .isEqualTo(
+                        "com.android.appsearch.flags.enable_set_publicly_visible_schema");
+    }
+
+    @Test
+    public void testFlagValue_enableEnterpriseGlobalSearchSession() {
+        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/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java
new file mode 100644
index 0000000..d4f3bf0
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/flags/RequiresFlagsEnabled.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.flags;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Shim for real RequiresFlagsEnabled defined in Framework.
+ *
+ * 

In Jetpack, this shim does nothing and exists only for code sync purpose. + * + *

In Framework, indicates that a specific test or class should be run only if all of the given + * feature flags are enabled in the device's current state. Enforced by the {@code CheckFlagsRule}. + * + *

This annotation works together with RequiresFlagsDisabled to define the value that is + * required of the flag by the test for the test to run. It is an error for either a method or class + * to require that a particular flag be both enabled and disabled. + * + *

If the value of a particular flag is required (by either {@code RequiresFlagsEnabled} or + * {@code RequiresFlagsDisabled}) by both the class and test method, then the values must be + * consistent. + * + *

If the value of a one flag is required by an annotation on the class, and the value of a + * different flag is required by an annotation of the method, then both requirements apply. + * + *

With {@code CheckFlagsRule}, test(s) will be skipped with 'assumption failed' when any of the + * required flag on the target Android platform is disabled. + * + *

Both {@code SetFlagsRule} and {@code CheckFlagsRule} will fail the test if a particular flag + * is both set (with {@code EnableFlags} or {@code DisableFlags}) and required (with {@code + * RequiresFlagsEnabled} or {@code RequiresFlagsDisabled}). + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface RequiresFlagsEnabled { + /** + * The list of the feature flags that require to be enabled. Each item is the full flag name + * with the format {package_name}.{flag_name}. + */ + String[] value(); +}

diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
index d8cc9bc..0900ec8 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/GenericDocumentParcelTest.java
@@ -20,9 +20,15 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.Parcel;
+
+import androidx.appsearch.app.EmbeddingVector;
+
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 /** Tests for {@link androidx.appsearch.app.GenericDocument} related SafeParcels. */
@@ -34,6 +40,8 @@
         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()};
 
@@ -52,6 +60,9 @@
         assertThat(new PropertyParcel.Builder("name").setBytesValues(
                 bytesValues).build().getBytesValues()).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()).isEqualTo(
                 Arrays.copyOf(docValues, docValues.length));
@@ -85,14 +96,14 @@
         builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
         GenericDocumentParcel genericDocumentParcel = builder.build();
 
-        PropertyParcel[] properties = genericDocumentParcel.getProperties();
+        List properties = genericDocumentParcel.getProperties();
         Map propertyMap = genericDocumentParcel.getPropertyMap();
         PropertyParcel longArrayProperty = new PropertyParcel.Builder(
                 /*name=*/ "longArray").setLongValues(longArray).build();
         PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
                 /*name=*/ "stringArray").setStringValues(stringArray).build();
 
-        assertThat(properties).asList().containsExactly(longArrayProperty, stringArrayProperty);
+        assertThat(properties).containsExactly(longArrayProperty, stringArrayProperty);
         assertThat(propertyMap).containsExactly("longArray", longArrayProperty,
                 "stringArray", stringArrayProperty);
     }
@@ -106,8 +117,10 @@
                         /*schemaType=*/ "schemaType");
         long[] longArray = new long[]{1L, 2L, 3L};
         String[] stringArray = new String[]{"hello", "world", "!"};
+        List parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
         builder.putInPropertyMap(/*name=*/ "longArray", /*values=*/ longArray);
         builder.putInPropertyMap(/*name=*/ "stringArray", /*values=*/ stringArray);
+        builder.setParentTypes(parentTypes);
         GenericDocumentParcel genericDocumentParcel = builder.build();
 
         GenericDocumentParcel genericDocumentParcelCopy =
@@ -124,9 +137,130 @@
                 genericDocumentParcel.getTtlMillis());
         assertThat(genericDocumentParcelCopy.getScore()).isEqualTo(
                 genericDocumentParcel.getScore());
+        assertThat(genericDocumentParcelCopy.getParentTypes()).isEqualTo(
+                genericDocumentParcel.getParentTypes());
+
         // Check it is a copy.
         assertThat(genericDocumentParcelCopy).isNotSameInstanceAs(genericDocumentParcel);
         assertThat(genericDocumentParcelCopy.getProperties()).isEqualTo(
                 genericDocumentParcel.getProperties());
     }
+
+    @Test
+    public void testGenericDocumentParcelWithParentTypes() {
+        GenericDocumentParcel.Builder builder =
+                new GenericDocumentParcel.Builder(
+                        /*namespace=*/ "namespace",
+                        /*id=*/ "id",
+                        /*schemaType=*/ "schemaType");
+        List parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
+
+        builder.setParentTypes(parentTypes);
+        GenericDocumentParcel genericDocumentParcel = builder.build();
+
+        assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
+    }
+
+    @Test
+    public void testGenericDocumentParcel_builderCanBeReused() {
+        GenericDocumentParcel.Builder builder =
+                new GenericDocumentParcel.Builder(
+                        /*namespace=*/ "namespace",
+                        /*id=*/ "id",
+                        /*schemaType=*/ "schemaType");
+        long[] longArray = new long[]{1L, 2L, 3L};
+        String[] stringArray = new String[]{"hello", "world", "!"};
+        List parentTypes = new ArrayList<>(Arrays.asList("parentType1", "parentType2"));
+        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[]{""});
+
+        PropertyParcel longArrayProperty = new PropertyParcel.Builder(
+                /*name=*/ "longArray").setLongValues(longArray).build();
+        PropertyParcel stringArrayProperty = new PropertyParcel.Builder(
+                /*name=*/ "stringArray").setStringValues(stringArray).build();
+        assertThat(genericDocumentParcel.getParentTypes()).isEqualTo(parentTypes);
+        assertThat(genericDocumentParcel.getPropertyMap()).containsExactly("longArray",
+                longArrayProperty, "stringArray", stringArrayProperty);
+    }
+
+    @Test
+    public void testRecreateFromParcelWithParentTypes() {
+        String[] stringArray = new String[]{"Hello", "world"};
+        long[] longArray = new long[]{1L, 2L};
+        double[] doubleArray = new double[]{1.1, 2.2};
+        boolean[] booleanArray = new boolean[]{true, false};
+        byte[][] bytesArray = new byte[][]{{1, 2}};
+        GenericDocumentParcel inDoc = new GenericDocumentParcel.Builder(
+                "namespace1", "id1", "schema1")
+                .setParentTypes(new ArrayList<>(Arrays.asList("Class1", "Class2")))
+                .setScore(42)
+                .setTtlMillis(43)
+                .setCreationTimestampMillis(44)
+                .putInPropertyMap("propStrings", stringArray)
+                .putInPropertyMap("propLongs", longArray)
+                .putInPropertyMap("propDoubles", doubleArray)
+                .putInPropertyMap("propBytes", bytesArray)
+                .putInPropertyMap("propBooleans", booleanArray)
+                .putInPropertyMap(
+                        "propDoc",
+                        new GenericDocumentParcel[]{
+                                new GenericDocumentParcel.Builder(
+                                        "namespace2", "id2", "schema2")
+                                        .putInPropertyMap("propStrings", new String[]{"Goodbye"})
+                                        .putInPropertyMap("propBytes", new byte[][]{{3, 4}})
+                                        .build()})
+                .build();
+
+        // Serialize the document
+        Parcel inParcel = Parcel.obtain();
+        inParcel.writeParcelable(inDoc, /*flags=*/ 0);
+        byte[] data = inParcel.marshall();
+        inParcel.recycle();
+
+        // Deserialize the document
+        Parcel outParcel = Parcel.obtain();
+        outParcel.unmarshall(data, 0, data.length);
+        outParcel.setDataPosition(0);
+        @SuppressWarnings("deprecation")
+        GenericDocumentParcel outDoc = outParcel.readParcelable(getClass().getClassLoader());
+        outParcel.recycle();
+
+        // Compare results
+        assertThat(outDoc.getId()).isEqualTo("id1");
+        assertThat(outDoc.getNamespace()).isEqualTo("namespace1");
+        assertThat(outDoc.getSchemaType()).isEqualTo("schema1");
+        assertThat(outDoc.getParentTypes()).isEqualTo(Arrays.asList("Class1", "Class2"));
+        assertThat(outDoc.getScore()).isEqualTo(42);
+        assertThat(outDoc.getTtlMillis()).isEqualTo(43);
+        assertThat(outDoc.getCreationTimestampMillis()).isEqualTo(44);
+
+        // Properties
+        Map propertyMap = outDoc.getPropertyMap();
+        assertThat(propertyMap.get("propStrings").getStringValues()).isEqualTo(stringArray);
+        assertThat(propertyMap.get("propLongs").getLongValues()).isEqualTo(longArray);
+        assertThat(propertyMap.get("propDoubles").getDoubleValues()).isEqualTo(doubleArray);
+        assertThat(propertyMap.get("propBytes").getBytesValues()).isEqualTo(bytesArray);
+        assertThat(propertyMap.get("propBooleans").getBooleanValues()).isEqualTo(booleanArray);
+
+        // Check inner doc.
+        GenericDocumentParcel[] innerDocs = propertyMap.get("propDoc").getDocumentValues();
+        assertThat(innerDocs).hasLength(1);
+        assertThat(innerDocs[0].getNamespace()).isEqualTo("namespace2");
+        assertThat(innerDocs[0].getId()).isEqualTo("id2");
+        assertThat(innerDocs[0].getSchemaType()).isEqualTo("schema2");
+        assertThat(innerDocs[0].getPropertyMap().get("propStrings").getStringValues()).isEqualTo(
+                new String[]{"Goodbye"});
+        assertThat(innerDocs[0].getPropertyMap().get("propBytes").getBytesValues()).isEqualTo(
+                new byte[][]{{3, 4}});
+
+        // Finally check equals and hashcode
+        assertThat(inDoc).isEqualTo(outDoc);
+        assertThat(inDoc.hashCode()).isEqualTo(outDoc.hashCode());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java
new file mode 100644
index 0000000..a5ce50b
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/safeparcel/PropertyParcelTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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 androidx.appsearch.safeparcel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class PropertyParcelTest {
+    @Test
+    public void testTwoDimensionByteArray_serializationSupported() {
+        int row = 20;
+        int col = 10;
+        byte[][] bytesArray = new byte[row][col];
+        for (int i = 0; i < row; ++i) {
+            for (int j = 0; j < col; ++j) {
+                bytesArray[i][j] = (byte) (i + j);
+            }
+        }
+
+        String propertyName = "propertyName";
+        PropertyParcel expectedPropertyParcel =
+                new PropertyParcel.Builder(propertyName).setBytesValues(bytesArray).build();
+        Parcel data = Parcel.obtain();
+        try {
+            data.writeParcelable(expectedPropertyParcel, /* flags= */ 0);
+            data.setDataPosition(0);
+            @SuppressWarnings("deprecation")
+            PropertyParcel actualPropertyParcel = data.readParcelable(
+                    PropertyParcelTest.class.getClassLoader());
+            assertThat(expectedPropertyParcel).isEqualTo(actualPropertyParcel);
+        } finally {
+            data.recycle();
+        }
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
index 59415fb..935c5bf 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/Document.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.EmbeddingVector;
 import androidx.appsearch.app.LongSerializer;
 import androidx.appsearch.app.StringSerializer;
 
@@ -228,7 +229,6 @@
          * 

If not specified, defaults to {@link * AppSearchSchema.StringPropertyConfig#INDEXING_TYPE_NONE} (the field will not be indexed * and cannot be queried). - * TODO(b/171857731) renamed to TermMatchType when using String-specific indexing config. */ @AppSearchSchema.StringPropertyConfig.IndexingType int indexingType() default AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE; @@ -540,6 +540,42 @@ } /** + * Configures an {@link EmbeddingVector} field of a class as a property known to AppSearch. + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.FIELD, ElementType.METHOD}) + @interface EmbeddingProperty { + /** + * The name of this property. This string is used to query against this property. + * + *

If not specified, the name of the field in the code will be used instead. + */ + String name() default ""; + + /** + * Configures how a property should be indexed so that it can be retrieved by queries. + * + *

If not specified, defaults to + * {@link AppSearchSchema.EmbeddingPropertyConfig#INDEXING_TYPE_NONE} (the field will not be + * indexed and cannot be queried). + */ + @AppSearchSchema.EmbeddingPropertyConfig.IndexingType int indexingType() + default AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE; + + /** + * Configures whether this property must be specified for the document to be valid. + * + *

This attribute does not apply to properties of a repeated type (e.g. a list). + * + *

Please make sure you understand the consequences of required fields on + * {@link androidx.appsearch.app.AppSearchSession#setSchemaAsync schema migration} before + * setting this attribute to {@code true}. + */ + boolean required() default false; + } + + /** * Marks a static method or a builder class directly as a builder producer. A builder class * should contain a "build()" method to construct the AppSearch document object and setter * methods to set field values.

diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
index 8263467..08a16d2 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchBatchResult.java
@@ -44,12 +44,14 @@
  * @see AppSearchSession#removeAsync
  */
 public final class AppSearchBatchResult {
-    @NonNull private final Map mSuccesses;
+    @NonNull private final Map
+            @androidx.appsearch.checker.nullness.qual.Nullable ValueType> mSuccesses;
     @NonNull private final Map> mFailures;
     @NonNull private final Map> mAll;
 
     AppSearchBatchResult(
-            @NonNull Map successes,
+            @NonNull Map
+                    successes,
             @NonNull Map> failures,
             @NonNull Map> all) {
         mSuccesses = Preconditions.checkNotNull(successes);
@@ -123,7 +125,8 @@
      * @param  The type of the result objects for successful results.
      */
     public static final class Builder {
-        private ArrayMap mSuccesses = new ArrayMap<>();
+        private ArrayMap
+                mSuccesses = new ArrayMap<>();
         private ArrayMap> mFailures = new ArrayMap<>();
         private ArrayMap> mAll = new ArrayMap<>();
         private boolean mBuilt = false;
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java
new file mode 100644
index 0000000..aa5326b
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironment.java
@@ -0,0 +1,73 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+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.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+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 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/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java
new file mode 100644
index 0000000..9ae8ac1
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchEnvironmentFactory.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * This is a factory class for implementations needed based on the environment.
+ *
+ * @exportToFramework:hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class AppSearchEnvironmentFactory {
+    private static volatile AppSearchEnvironment sAppSearchEnvironment;
+
+    /** Returns the singleton instance of {@link AppSearchEnvironment}. */
+    @NonNull
+    public static AppSearchEnvironment getEnvironmentInstance() {
+        AppSearchEnvironment localRef = sAppSearchEnvironment;
+        if (localRef == null) {
+            synchronized (AppSearchEnvironmentFactory.class) {
+                localRef = sAppSearchEnvironment;
+                if (localRef == null) {
+                    sAppSearchEnvironment = localRef =
+                            new JetpackAppSearchEnvironment();
+                }
+            }
+        }
+        return localRef;
+    }
+
+    /** Sets an instance of {@link AppSearchEnvironment}. for testing.*/
+    @VisibleForTesting
+    public static void setEnvironmentInstanceForTest(
+            @NonNull AppSearchEnvironment appSearchEnvironment) {
+        synchronized (AppSearchEnvironmentFactory.class) {
+            sAppSearchEnvironment = appSearchEnvironment;
+        }
+    }
+
+    private AppSearchEnvironmentFactory() {
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
index d8c79a5..86c8102 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchResult.java
@@ -22,6 +22,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
 import androidx.appsearch.util.LogUtil;
 import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
@@ -54,6 +56,7 @@
             RESULT_SECURITY_ERROR,
             RESULT_DENIED,
             RESULT_RATE_LIMITED,
+            RESULT_TIMED_OUT
     })
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
@@ -101,20 +104,22 @@
     /**
      * The requested operation is denied for the caller. This error is logged and returned for
      * denylist rejections.
-     * 
      */
-    // 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.
-     * 
+     * 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.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    // 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;
 
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index a4fd79ef..b300d3e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -18,6 +18,8 @@
 
 import android.annotation.SuppressLint;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
@@ -27,7 +29,15 @@
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.appsearch.exceptions.IllegalSchemaException;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.PropertyConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;
+import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.AppSearchSchemaCreator;
 import androidx.appsearch.util.IndentingStringBuilder;
 import androidx.collection.ArraySet;
 import androidx.core.util.ObjectsCompat;
@@ -41,6 +51,7 @@
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -52,28 +63,36 @@
  *
  * @see AppSearchSession#setSchemaAsync
  */
-public final class AppSearchSchema {
-    private static final String SCHEMA_TYPE_FIELD = "schemaType";
-    private static final String PROPERTIES_FIELD = "properties";
-    private static final String PARENT_TYPES_FIELD = "parentTypes";
-
-    private final Bundle mBundle;
-
-    /** @exportToFramework:hide */
[email protected](creator = "AppSearchSchemaCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class AppSearchSchema extends AbstractSafeParcelable {
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public AppSearchSchema(@NonNull Bundle bundle) {
-        Preconditions.checkNotNull(bundle);
-        mBundle = bundle;
-    }
-
-    /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
     @NonNull
-    public Bundle getBundle() {
-        return mBundle;
+    public static final Parcelable.Creator CREATOR = new AppSearchSchemaCreator();
+
+    @Field(id = 1, getter = "getSchemaType")
+    private final String mSchemaType;
+
+    @Field(id = 2)
+    final List mPropertyConfigParcels;
+
+    @Field(id = 3, getter = "getParentTypes")
+    private final List mParentTypes;
+
+    @Field(id = 4, getter = "getDescription")
+    private final String mDescription;
+
+    @Constructor
+    AppSearchSchema(
+            @Param(id = 1) @NonNull String schemaType,
+            @Param(id = 2) @NonNull List propertyConfigParcels,
+            @Param(id = 3) @NonNull List parentTypes,
+            @Param(id = 4) @NonNull String description) {
+        mSchemaType = Objects.requireNonNull(schemaType);
+        mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels);
+        mParentTypes = Objects.requireNonNull(parentTypes);
+        mDescription = Objects.requireNonNull(description);
     }
 
     @Override
@@ -88,7 +107,7 @@
      * Appends a debugging string for the {@link AppSearchSchema} instance to the given string
      * builder.
      *
-     * @param builder     the builder to append to.
+     * @param builder the builder to append to.
      */
     private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
         Preconditions.checkNotNull(builder);
@@ -96,6 +115,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 = getProperties()
@@ -121,7 +141,22 @@
     /** Returns the name of this schema type, such as Email. */
     @NonNull
     public String getSchemaType() {
-        return mBundle.getString(SCHEMA_TYPE_FIELD, "");
+        return mSchemaType;
+    }
+
+    /**
+     * Returns a natural language description of this schema type.
+     *
+     * 

Ex. The description for an Email type could be "A type of electronic message". + * + *

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; } /** @@ -130,35 +165,25 @@ *

This method creates a new list when called. */ @NonNull - @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"}) + @SuppressWarnings({"MixedMutabilityReturnType"}) public List getProperties() { - ArrayList propertyBundles = - mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD); - if (propertyBundles == null || propertyBundles.isEmpty()) { + if (mPropertyConfigParcels.isEmpty()) { return Collections.emptyList(); } - List ret = new ArrayList<>(propertyBundles.size()); - for (int i = 0; i < propertyBundles.size(); i++) { - ret.add(PropertyConfig.fromBundle(propertyBundles.get(i))); + List ret = new ArrayList<>(mPropertyConfigParcels.size()); + for (int i = 0; i < mPropertyConfigParcels.size(); i++) { + ret.add(PropertyConfig.fromParcel(mPropertyConfigParcels.get(i))); } return ret; } /** * 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 getParentTypes() { - List parentTypes = mBundle.getStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD); - if (parentTypes == null) { - return Collections.emptyList(); - } - return Collections.unmodifiableList(parentTypes); + return Collections.unmodifiableList(mParentTypes); } @Override @@ -173,6 +198,9 @@ if (!getSchemaType().equals(otherSchema.getSchemaType())) { return false; } + if (!getDescription().equals(otherSchema.getDescription())) { + return false; + } if (!getParentTypes().equals(otherSchema.getParentTypes())) { return false; } @@ -181,21 +209,51 @@ @Override public int hashCode() { - return ObjectsCompat.hash(getSchemaType(), getProperties(), getParentTypes()); + return ObjectsCompat.hash( + getSchemaType(), + getProperties(), + getParentTypes(), + getDescription()); + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + AppSearchSchemaCreator.writeToParcel(this, dest, flags); } /** Builder for {@link AppSearchSchema objects}. */ public static final class Builder { private final String mSchemaType; - private ArrayList mPropertyBundles = new ArrayList<>(); + private String mDescription = ""; + private ArrayList mPropertyConfigParcels = new ArrayList<>(); private LinkedHashSet mParentTypes = new LinkedHashSet<>(); private final Set mPropertyNames = new ArraySet<>(); private boolean mBuilt = false; /** Creates a new {@link AppSearchSchema.Builder}. */ public Builder(@NonNull String schemaType) { - Preconditions.checkNotNull(schemaType); - mSchemaType = schemaType; + mSchemaType = Preconditions.checkNotNull(schemaType); + } + + /** + * Sets a natural language description of this schema type. + * + *

For more details about the description field, see {@link + * AppSearchSchema#getDescription}. + */ + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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. */ @@ -208,7 +266,7 @@ if (!mPropertyNames.add(name)) { throw new IllegalSchemaException("Property defined more than once: " + name); } - mPropertyBundles.add(propertyConfig.mBundle); + mPropertyConfigParcels.add(propertyConfig.mPropertyConfigParcel); return this; } @@ -273,11 +331,9 @@ */ @CanIgnoreReturnValue @NonNull - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_PARENT_TYPE) - // @exportToFramework:endStrip() public AppSearchSchema.Builder addParentType(@NonNull String parentSchemaType) { Preconditions.checkNotNull(parentSchemaType); resetIfBuilt(); @@ -288,18 +344,16 @@ /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */ @NonNull public AppSearchSchema build() { - Bundle bundle = new Bundle(); - bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType); - bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles); - bundle.putStringArrayList(AppSearchSchema.PARENT_TYPES_FIELD, - new ArrayList<>(mParentTypes)); mBuilt = true; - return new AppSearchSchema(bundle); + return new AppSearchSchema(mSchemaType, + mPropertyConfigParcels, + new ArrayList<>(mParentTypes), + mDescription); } private void resetIfBuilt() { if (mBuilt) { - mPropertyBundles = new ArrayList<>(mPropertyBundles); + mPropertyConfigParcels = new ArrayList<>(mPropertyConfigParcels); mParentTypes = new LinkedHashSet<>(mParentTypes); mBuilt = false; } @@ -313,10 +367,6 @@ * a property. */ public abstract static class PropertyConfig { - static final String NAME_FIELD = "name"; - static final String DATA_TYPE_FIELD = "dataType"; - static final String CARDINALITY_FIELD = "cardinality"; - /** * Physical data-types of the contents of the property. * @@ -333,28 +383,47 @@ DATA_TYPE_BOOLEAN, DATA_TYPE_BYTES, DATA_TYPE_DOCUMENT, + DATA_TYPE_EMBEDDING, }) @Retention(RetentionPolicy.SOURCE) - public @interface DataType {} + public @interface DataType { + } - /** @exportToFramework:hide */ + /** + * Constant value for String data type. + * + * @exportToFramework:hide + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_STRING = 1; - /** @exportToFramework:hide */ + /** + * Constant value for Long data type. + * + * @exportToFramework:hide + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_LONG = 2; - /** @exportToFramework:hide */ + /** + * Constant value for Double data type. + * + * @exportToFramework:hide + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_DOUBLE = 3; - /** @exportToFramework:hide */ + /** + * Constant value for Boolean data type. + * + * @exportToFramework:hide + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_BOOLEAN = 4; /** * Unstructured BLOB. + * * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -364,12 +433,21 @@ * Indicates that the property is itself a {@link GenericDocument}, making it part of a * hierarchical schema. Any property using this DataType MUST have a valid * {@link PropertyConfig#getSchemaType}. + * * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static final int DATA_TYPE_DOCUMENT = 6; /** + * Indicates that the property is an {@link EmbeddingVector}. + * + * @exportToFramework:hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static final int DATA_TYPE_EMBEDDING = 7; + + /** * The cardinality of the property (whether it is required, optional or repeated). * *

NOTE: The integer values of these constants must match the proto enum constants in @@ -384,7 +462,8 @@ CARDINALITY_REQUIRED, }) @Retention(RetentionPolicy.SOURCE) - public @interface Cardinality {} + public @interface Cardinality { + } /** Any number of items (including zero) [0...*]. */ public static final int CARDINALITY_REPEATED = 1; @@ -395,13 +474,10 @@ /** Exactly one value [1]. */ public static final int CARDINALITY_REQUIRED = 3; - final Bundle mBundle; + final PropertyConfigParcel mPropertyConfigParcel; - @Nullable - private Integer mHashCode; - - PropertyConfig(@NonNull Bundle bundle) { - mBundle = Preconditions.checkNotNull(bundle); + PropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + mPropertyConfigParcel = Preconditions.checkNotNull(propertyConfigParcel); } @Override @@ -416,7 +492,7 @@ * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the * given string builder. * - * @param builder the builder to append to. + * @param builder the builder to append to. */ void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) { Preconditions.checkNotNull(builder); @@ -424,6 +500,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) @@ -469,6 +546,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"); } @@ -479,7 +559,24 @@ /** Returns the name of this property. */ @NonNull public String getName() { - return mBundle.getString(NAME_FIELD, ""); + return mPropertyConfigParcel.getName(); + } + + /** + * Returns a natural language description of this property. + * + *

Ex. The description for the "homeAddress" property of a "Person" type could be "the + * address at which this person lives". + * + *

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(); } /** @@ -490,7 +587,7 @@ @RestrictTo(RestrictTo.Scope.LIBRARY) @DataType public int getDataType() { - return mBundle.getInt(DATA_TYPE_FIELD, -1); + return mPropertyConfigParcel.getDataType(); } /** @@ -498,7 +595,7 @@ */ @Cardinality public int getCardinality() { - return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL); + return mPropertyConfigParcel.getCardinality(); } @Override @@ -510,15 +607,12 @@ return false; } PropertyConfig otherProperty = (PropertyConfig) other; - return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle); + return ObjectsCompat.equals(mPropertyConfigParcel, otherProperty.mPropertyConfigParcel); } @Override public int hashCode() { - if (mHashCode == null) { - mHashCode = BundleUtil.deepHashCode(mBundle); - } - return mHashCode; + return mPropertyConfigParcel.hashCode(); } /** @@ -528,41 +622,40 @@ *

The bundle is not cloned. * * @throws IllegalArgumentException if the bundle does no contain a recognized - * value in its {@code DATA_TYPE_FIELD}. + * value in its {@code DATA_TYPE_FIELD}. * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull - public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) { - switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) { + public static PropertyConfig fromParcel( + @NonNull PropertyConfigParcel propertyConfigParcel) { + Preconditions.checkNotNull(propertyConfigParcel); + switch (propertyConfigParcel.getDataType()) { case PropertyConfig.DATA_TYPE_STRING: - return new StringPropertyConfig(propertyBundle); + return new StringPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_LONG: - return new LongPropertyConfig(propertyBundle); + return new LongPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_DOUBLE: - return new DoublePropertyConfig(propertyBundle); + return new DoublePropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_BOOLEAN: - return new BooleanPropertyConfig(propertyBundle); + return new BooleanPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_BYTES: - return new BytesPropertyConfig(propertyBundle); + return new BytesPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_DOCUMENT: - return new DocumentPropertyConfig(propertyBundle); + return new DocumentPropertyConfig(propertyConfigParcel); + case PropertyConfig.DATA_TYPE_EMBEDDING: + return new EmbeddingPropertyConfig(propertyConfigParcel); default: throw new IllegalArgumentException( "Unsupported property bundle of type " - + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD) - + "; contents: " + propertyBundle); + + propertyConfigParcel.getDataType() + + "; contents: " + propertyConfigParcel); } } } /** Configuration for a property of type String in a Document. */ public static final class StringPropertyConfig extends PropertyConfig { - private static final String INDEXING_TYPE_FIELD = "indexingType"; - private static final String TOKENIZER_TYPE_FIELD = "tokenizerType"; - private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType"; - private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation"; - /** * Encapsulates the configurations on how AppSearch should query/index these terms. * @exportToFramework:hide @@ -574,7 +667,8 @@ INDEXING_TYPE_PREFIXES, }) @Retention(RetentionPolicy.SOURCE) - public @interface IndexingType {} + public @interface IndexingType { + } /** Content in this property will not be tokenized or indexed. */ public static final int INDEXING_TYPE_NONE = 0; @@ -611,7 +705,8 @@ TOKENIZER_TYPE_RFC822 }) @Retention(RetentionPolicy.SOURCE) - public @interface TokenizerType {} + public @interface TokenizerType { + } /** * This value indicates that no tokens should be extracted from this property. @@ -646,11 +741,9 @@ *

It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */ -// @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.VERBATIM_SEARCH) -// @exportToFramework:endStrip() public static final int TOKENIZER_TYPE_VERBATIM = 2; /** @@ -663,11 +756,9 @@ *

It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */ -// @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.TOKENIZER_TYPE_RFC822) -// @exportToFramework:endStrip() public static final int TOKENIZER_TYPE_RFC822 = 3; /** @@ -684,7 +775,8 @@ }) @RestrictTo(RestrictTo.Scope.LIBRARY) @Retention(RetentionPolicy.SOURCE) - public @interface JoinableValueType {} + public @interface JoinableValueType { + } /** Content in this property is not joinable. */ public static final int JOINABLE_VALUE_TYPE_NONE = 0; @@ -700,27 +792,37 @@ * {@link PropertyConfig#CARDINALITY_REQUIRED}. * */ - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.JOIN_SPEC_AND_QUALIFIED_ID) - // @exportToFramework:endStrip() public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1; - StringPropertyConfig(@NonNull Bundle bundle) { - super(bundle); + StringPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + super(propertyConfigParcel); } /** Returns how the property is indexed. */ - @IndexingType + @StringPropertyConfig.IndexingType public int getIndexingType() { - return mBundle.getInt(INDEXING_TYPE_FIELD); + 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 mBundle.getInt(TOKENIZER_TYPE_FIELD); + StringIndexingConfigParcel indexingConfigParcel = + mPropertyConfigParcel.getStringIndexingConfigParcel(); + if (indexingConfigParcel == null) { + return TOKENIZER_TYPE_NONE; + } + + return indexingConfigParcel.getTokenizerType(); } /** @@ -728,27 +830,27 @@ */ @JoinableValueType public int getJoinableValueType() { - return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE); - } + 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. - * - * @see JoinSpec - * @ - */ - public boolean getDeletionPropagation() { - return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false); + return joinableConfigParcel.getJoinableValueType(); } /** Builder for {@link StringPropertyConfig}. */ public static final class Builder { private final String mPropertyName; - @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; - @IndexingType private int mIndexingType = INDEXING_TYPE_NONE; - @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE; - @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE; + private String mDescription = ""; + @Cardinality + private int mCardinality = CARDINALITY_OPTIONAL; + @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; /** Creates a new {@link StringPropertyConfig.Builder}. */ @@ -757,6 +859,24 @@ } /** + * Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). * *

If this method is not called, the default cardinality is @@ -781,7 +901,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; @@ -830,25 +951,6 @@ } /** - * Configures whether or not documents in this schema will be removed when the document - * referred to by this property is deleted. - * - *

Requires that a joinable value type is set. - * @ - */ - @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation - @NonNull - // @exportToFramework:startStrip() - @RequiresFeature( - enforcement = "androidx.appsearch.app.Features#isFeatureSupported", - name = Features.SCHEMA_SET_DELETION_PROPAGATION) - // @exportToFramework:endStrip() - public Builder setDeletionPropagation(boolean deletionPropagation) { - mDeletionPropagation = deletionPropagation; - return this; - } - - /** * Constructs a new {@link StringPropertyConfig} from the contents of this builder. */ @NonNull @@ -868,15 +970,17 @@ Preconditions.checkState(!mDeletionPropagation, "Cannot set deletion " + "propagation without setting a joinable value type"); } - Bundle bundle = new Bundle(); - bundle.putString(NAME_FIELD, mPropertyName); - bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING); - bundle.putInt(CARDINALITY_FIELD, mCardinality); - bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType); - bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType); - bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType); - bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation); - return new StringPropertyConfig(bundle); + PropertyConfigParcel.StringIndexingConfigParcel stringConfigParcel = + new StringIndexingConfigParcel(mIndexingType, mTokenizerType); + JoinableConfigParcel joinableConfigParcel = + new JoinableConfigParcel(mJoinableValueType, mDeletionPropagation); + return new StringPropertyConfig( + PropertyConfigParcel.createForString( + mPropertyName, + mDescription, + mCardinality, + stringConfigParcel, + joinableConfigParcel)); } } @@ -886,7 +990,7 @@ * *

This appends fields specific to a {@link StringPropertyConfig} instance. * - * @param builder the builder to append to. + * @param builder the builder to append to. */ void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) { switch (getIndexingType()) { @@ -935,11 +1039,10 @@ /** Configuration for a property containing a 64-bit integer. */ public static final class LongPropertyConfig extends PropertyConfig { - private static final String INDEXING_TYPE_FIELD = "indexingType"; - /** * Encapsulates the configurations on how AppSearch should query/index these 64-bit * integers. + * * @exportToFramework:hide */ @IntDef(value = { @@ -948,7 +1051,8 @@ }) @RestrictTo(RestrictTo.Scope.LIBRARY) @Retention(RetentionPolicy.SOURCE) - public @interface IndexingType {} + public @interface IndexingType { + } /** Content in this property will not be indexed. */ public static final int INDEXING_TYPE_NONE = 0; @@ -959,28 +1063,34 @@ * *

For example, a property with 1024 should match numeric search range query [0, 2000]. */ - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.NUMERIC_SEARCH) - // @exportToFramework:endStrip() public static final int INDEXING_TYPE_RANGE = 1; - LongPropertyConfig(@NonNull Bundle bundle) { - super(bundle); + LongPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + super(propertyConfigParcel); } /** Returns how the property is indexed. */ - @IndexingType + @LongPropertyConfig.IndexingType public int getIndexingType() { - return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE); + 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; - @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; - @IndexingType private int mIndexingType = INDEXING_TYPE_NONE; + private String mDescription = ""; + @Cardinality + private int mCardinality = CARDINALITY_OPTIONAL; + @LongPropertyConfig.IndexingType + private int mIndexingType = INDEXING_TYPE_NONE; /** Creates a new {@link LongPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { @@ -988,6 +1098,24 @@ } /** + * Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). * *

If this method is not called, the default cardinality is @@ -1012,7 +1140,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; @@ -1022,12 +1151,9 @@ /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */ @NonNull public LongPropertyConfig build() { - Bundle bundle = new Bundle(); - bundle.putString(NAME_FIELD, mPropertyName); - bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG); - bundle.putInt(CARDINALITY_FIELD, mCardinality); - bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType); - return new LongPropertyConfig(bundle); + return new LongPropertyConfig( + PropertyConfigParcel.createForLong( + mPropertyName, mDescription, mCardinality, mIndexingType)); } } @@ -1037,7 +1163,7 @@ * *

This appends fields specific to a {@link LongPropertyConfig} instance. * - * @param builder the builder to append to. + * @param builder the builder to append to. */ void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) { switch (getIndexingType()) { @@ -1055,14 +1181,16 @@ /** Configuration for a property containing a double-precision decimal number. */ public static final class DoublePropertyConfig extends PropertyConfig { - DoublePropertyConfig(@NonNull Bundle bundle) { - super(bundle); + DoublePropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + super(propertyConfigParcel); } /** Builder for {@link DoublePropertyConfig}. */ public static final class Builder { private final String mPropertyName; - @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; + private String mDescription = ""; + @Cardinality + private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link DoublePropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { @@ -1070,6 +1198,24 @@ } /** + * Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). * *

If this method is not called, the default cardinality is @@ -1088,25 +1234,25 @@ /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */ @NonNull public DoublePropertyConfig build() { - Bundle bundle = new Bundle(); - bundle.putString(NAME_FIELD, mPropertyName); - bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE); - bundle.putInt(CARDINALITY_FIELD, mCardinality); - return new DoublePropertyConfig(bundle); + return new DoublePropertyConfig( + PropertyConfigParcel.createForDouble( + mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing a boolean. */ public static final class BooleanPropertyConfig extends PropertyConfig { - BooleanPropertyConfig(@NonNull Bundle bundle) { - super(bundle); + BooleanPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + super(propertyConfigParcel); } /** Builder for {@link BooleanPropertyConfig}. */ public static final class Builder { private final String mPropertyName; - @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; + private String mDescription = ""; + @Cardinality + private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BooleanPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { @@ -1114,6 +1260,24 @@ } /** + Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). * *

If this method is not called, the default cardinality is @@ -1132,25 +1296,25 @@ /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */ @NonNull public BooleanPropertyConfig build() { - Bundle bundle = new Bundle(); - bundle.putString(NAME_FIELD, mPropertyName); - bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN); - bundle.putInt(CARDINALITY_FIELD, mCardinality); - return new BooleanPropertyConfig(bundle); + return new BooleanPropertyConfig( + PropertyConfigParcel.createForBoolean( + mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing a byte array. */ public static final class BytesPropertyConfig extends PropertyConfig { - BytesPropertyConfig(@NonNull Bundle bundle) { - super(bundle); + BytesPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + super(propertyConfigParcel); } /** Builder for {@link BytesPropertyConfig}. */ public static final class Builder { private final String mPropertyName; - @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; + private String mDescription = ""; + @Cardinality + private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BytesPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { @@ -1158,6 +1322,24 @@ } /** + * Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). * *

If this method is not called, the default cardinality is @@ -1178,30 +1360,23 @@ */ @NonNull public BytesPropertyConfig build() { - Bundle bundle = new Bundle(); - bundle.putString(NAME_FIELD, mPropertyName); - bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES); - bundle.putInt(CARDINALITY_FIELD, mCardinality); - return new BytesPropertyConfig(bundle); + return new BytesPropertyConfig( + PropertyConfigParcel.createForBytes( + mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing another Document. */ public static final class DocumentPropertyConfig extends PropertyConfig { - private static final String SCHEMA_TYPE_FIELD = "schemaType"; - private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties"; - private static final String INDEXABLE_NESTED_PROPERTIES_LIST_FIELD = - "indexableNestedPropertiesList"; - - DocumentPropertyConfig(@NonNull Bundle bundle) { - super(bundle); + DocumentPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { + super(propertyConfigParcel); } /** Returns the logical schema-type of the contents of this document property. */ @NonNull public String getSchemaType() { - return Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD)); + return Preconditions.checkNotNull(mPropertyConfigParcel.getSchemaType()); } /** @@ -1215,24 +1390,33 @@ * indexing a subset of properties from the nested document. */ public boolean shouldIndexNestedProperties() { - return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD); + DocumentIndexingConfigParcel indexingConfigParcel = + mPropertyConfigParcel.getDocumentIndexingConfigParcel(); + if (indexingConfigParcel == null) { + return false; + } + + return indexingConfigParcel.shouldIndexNestedProperties(); } /** * 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 getIndexableNestedProperties() { + DocumentIndexingConfigParcel indexingConfigParcel = + mPropertyConfigParcel.getDocumentIndexingConfigParcel(); + if (indexingConfigParcel == null) { + return Collections.emptyList(); + } + List indexableNestedPropertiesList = - mBundle.getStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD); + indexingConfigParcel.getIndexableNestedPropertiesList(); if (indexableNestedPropertiesList == null) { return Collections.emptyList(); } + return Collections.unmodifiableList(indexableNestedPropertiesList); } @@ -1240,7 +1424,9 @@ public static final class Builder { private final String mPropertyName; private final String mSchemaType; - @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; + private String mDescription = ""; + @Cardinality + private int mCardinality = CARDINALITY_OPTIONAL; private boolean mShouldIndexNestedProperties = false; private final Set mIndexableNestedPropertiesList = new ArraySet<>(); @@ -1250,9 +1436,9 @@ * @param propertyName The logical name of the property in the schema, which will be * used as the key for this property in * {@link GenericDocument.Builder#setPropertyDocument}. - * @param schemaType The type of documents which will be stored in this property. - * Documents of different types cannot be mixed into a single - * property. + * @param schemaType The type of documents which will be stored in this property. + * Documents of different types cannot be mixed into a single + * property. */ public Builder(@NonNull String propertyName, @NonNull String schemaType) { mPropertyName = Preconditions.checkNotNull(propertyName); @@ -1260,6 +1446,24 @@ } /** + * Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). * *

If this method is not called, the default cardinality is @@ -1297,19 +1501,13 @@ * Adds one or more properties for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection) - * - * */ + @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @NonNull - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) - // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedProperties( @NonNull String... indexableNestedProperties) { Preconditions.checkNotNull(indexableNestedProperties); @@ -1320,20 +1518,14 @@ * Adds one or more property paths for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection) - * - * */ + @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) - // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths( @NonNull PropertyPath... indexableNestedPropertyPaths) { Preconditions.checkNotNull(indexableNestedPropertyPaths); @@ -1371,11 +1563,9 @@ */ @CanIgnoreReturnValue @NonNull - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) - // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedProperties( @NonNull Collection indexableNestedProperties) { Preconditions.checkNotNull(indexableNestedProperties); @@ -1387,20 +1577,14 @@ * Adds one or more property paths for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection) - * - * */ + @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES) - // @exportToFramework:endStrip() public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths( @NonNull Collection indexableNestedPropertyPaths) { Preconditions.checkNotNull(indexableNestedPropertyPaths); @@ -1426,15 +1610,14 @@ + "to be false when one or more indexableNestedProperties are " + "provided."); } - Bundle bundle = new Bundle(); - bundle.putString(NAME_FIELD, mPropertyName); - bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT); - bundle.putInt(CARDINALITY_FIELD, mCardinality); - bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties); - bundle.putStringArrayList(INDEXABLE_NESTED_PROPERTIES_LIST_FIELD, - new ArrayList<>(mIndexableNestedPropertiesList)); - bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType); - return new DocumentPropertyConfig(bundle); + return new DocumentPropertyConfig( + PropertyConfigParcel.createForDocument( + mPropertyName, + mDescription, + mCardinality, + mSchemaType, + new DocumentIndexingConfigParcel(mShouldIndexNestedProperties, + new ArrayList<>(mIndexableNestedPropertiesList)))); } } @@ -1444,7 +1627,7 @@ * *

This appends fields specific to a {@link DocumentPropertyConfig} instance. * - * @param builder the builder to append to. + * @param builder the builder to append to. */ void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) { builder @@ -1459,4 +1642,133 @@ builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); } } + + /** + * Configuration for a property of type {@link EmbeddingVector} in a Document. + */ + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) + @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. + * + * @exportToFramework:hide + */ + @IntDef(value = { + INDEXING_TYPE_NONE, + INDEXING_TYPE_SIMILARITY + }) + @RestrictTo(RestrictTo.Scope.LIBRARY) + @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. + * + *

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 = Preconditions.checkNotNull(propertyName); + } + + /** + * Sets a natural language description of this property. + * + *

For more details about the description field, see {@link + * AppSearchSchema.PropertyConfig#getDescription}. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_SET_DESCRIPTION) + @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). + * + *

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. + * + *

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/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 05d1815..1e1ab76 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -176,7 +176,7 @@
      * 

The newly added custom functions covered by this feature are: *

    *
  • createList(String...)
  • - *
  • search(String, List)
  • + *
  • search(String, {@code List})
  • *
  • propertyDefined(String)
  • *
* @@ -192,8 +192,9 @@ *

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 - * actually hold any values for this property. + * the document's type defines the specified property. Unlike the "hasProperty" function + * below, this function does NOT require that the document actually hold any values for this + * property. * *

{@link Features#NUMERIC_SEARCH}: This feature covers numeric search expressions. In the * query language, the values of properties that have @@ -209,6 +210,68 @@ * *

Ex. `"foo/bar" OR baz` will ensure that 'foo/bar' is treated as a single 'verbatim' token. * + *

{@link Features#LIST_FILTER_HAS_PROPERTY_FUNCTION}: This feature covers the + * "hasProperty" function in query expressions, which takes a string specifying the property + * of interest and matches all documents that hold values for this property. Not to be + * confused with the "propertyDefined" function, which checks whether a document's schema + * has defined the property, instead of whether a document itself has this property. + * + *

Ex. `foo hasProperty("sender.name")` will return all documents that have the term "foo" + * AND have values in the property "sender.name". Consider two documents, documentA and + * documentB, of the same schema with an optional property "sender.name". If documentA sets + * "foo" in this property but documentB does not, then `hasProperty("sender.name")` will only + * match documentA. However, `propertyDefined("sender.name")` will match both documentA and + * documentB, regardless of whether a value is actually set. + * + *

{@link Features#SCHEMA_EMBEDDING_PROPERTY_CONFIG}: This feature covers the + * "semanticSearch" and "getSearchSpecEmbedding" functions in query expressions, which are + * used for semantic search. + * + *

Usage: semanticSearch(getSearchSpecEmbedding({embedding_index}), {low}, {high}, {metric}) + *

    + *
  • semanticSearch matches all documents that have at least one embedding vector with + * a matching model signature (see {@link EmbeddingVector#getModelSignature()}) and a + * similarity score within the range specified based on the provided metric.
  • + *
  • getSearchSpecEmbedding({embedding_index}) retrieves the embedding search passed in + * {@link SearchSpec.Builder#addSearchEmbeddings} based on the index specified, which + * starts from 0.
  • + *
  • "low" and "high" are floating point numbers that specify the similarity score + * range. If omitted, they default to negative and positive infinity, respectively.
  • + *
  • "metric" is a string value that specifies how embedding similarities should be + * calculated. If omitted, it defaults to the metric specified in + * {@link SearchSpec.Builder#setDefaultEmbeddingSearchMetricType(int)}. Possible + * values:
  • + *
      + *
    • "COSINE"
    • + *
    • "DOT_PRODUCT"
    • + *
    • "EUCLIDEAN"
    • + *
    + *
+ * + *

Examples: + *

    + *
  • Basic: semanticSearch(getSearchSpecEmbedding(0), 0.5, 1, "COSINE")
  • + *
  • With a property restriction: + * property1:semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)
  • + *
  • Hybrid: foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)
  • + *
  • Complex: (foo OR semanticSearch(getSearchSpecEmbedding(0), 0.5, 1)) AND bar
  • + *
+ * + *

{@link Features#LIST_FILTER_TOKENIZE_FUNCTION}: This feature covers the + * "tokenize" function in query expressions, which takes a string and treats the entire string + * as plain text. This string is then segmented, normalized and stripped of punctuation-only + * segments. The remaining tokens are then AND'd together. This function is useful for callers + * who wish to provide user input, but want to ensure that that user input does not invoke any + * query operators. + * + *

Ex. `foo OR tokenize("bar OR baz.")`. The string "bar OR baz." will be segmented into + * "bar", "OR", "baz", ".". Punctuation is removed and the segments are normalized to "bar", + * "or", "baz". This query will be equivalent to `foo OR (bar AND or AND baz)`. + * + *

Ex. `tokenize("\"bar\" OR \\baz")`. Quotation marks and escape characters must be escaped. + * This query will be segmented into "\"", "bar", "\"", "OR", "\", "baz". Once stripped of + * punctuation and normalized, this will be equivalent to the query `bar AND or AND baz`. + * *

The availability of each of these features can be checked by calling * {@link Features#isFeatureSupported} with the desired feature. * @@ -223,6 +286,8 @@ * match type, etc. * @return a {@link SearchResults} object for retrieved matched documents. */ + // TODO(b/326656531): Refine the javadoc to provide guidance on the best practice of + // embedding searches and how to select an appropriate metric. @NonNull SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec);

diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java
new file mode 100644
index 0000000..1addca7
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EmbeddingVector.java
@@ -0,0 +1,125 @@
+/*
+ * 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 androidx.appsearch.app;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingVectorCreator;
+import androidx.core.util.Preconditions;
+
+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.
+ *
+ * 

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 + */ +@RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) +@FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) [email protected](creator = "EmbeddingVectorCreator") +@SuppressWarnings("HiddenSuperclass") +public final class EmbeddingVector extends AbstractSafeParcelable { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @NonNull + public static final Parcelable.Creator 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 = Preconditions.checkNotNull(values); + if (mValues.length == 0) { + throw new IllegalArgumentException("Embedding values cannot be empty."); + } + mModelSignature = Preconditions.checkNotNull(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; + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + EmbeddingVectorCreator.writeToParcel(this, dest, flags); + } +}

diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java
new file mode 100644
index 0000000..eaaec59
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/EnterpriseGlobalSearchSession.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Provides a connection to all enterprise (work profile) AppSearch databases the querying
+ * application has been granted access to.
+ *
+ * 

This session can be created from any user profile but will only properly return results when + * created from the main profile. If the user is not the main profile or an associated work profile + * does not exist, queries will still successfully complete but with empty results. + * + *

Schemas must be explicitly tagged enterprise and may require additional permissions to be + * visible from an enterprise session. Retrieved documents may also have certain fields restricted + * or modified unlike if they were retrieved directly from {@link GlobalSearchSession} on the work + * profile. + * + *

All implementations of this interface must be thread safe. + * + * @see GlobalSearchSession + */ +@RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.ENTERPRISE_GLOBAL_SEARCH_SESSION) +public interface EnterpriseGlobalSearchSession { + /** + * 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 EnterpriseGlobalSearchSession} database. When a call is successful, the result will be + * returned in the successes section of the {@link AppSearchBatchResult} object in the callback. + * If the package doesn't exist, database doesn't exist, or if the calling package doesn't have + * access, these failures will be reflected as {@link AppSearchResult} objects with a + * RESULT_NOT_FOUND status code in the failures section of the {@link AppSearchBatchResult} + * object. + * + * @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 of the documents to retrieve. + */ + @NonNull + ListenableFuture> getByDocumentIdAsync( + @NonNull String packageName, + @NonNull String databaseName, + @NonNull GetByDocumentIdRequest request); + + /** + * Retrieves documents from all enterprise (work profile) AppSearch databases that the querying + * application has access to. + * + *

Applications can be granted access to documents by specifying + * {@link SetSchemaRequest.Builder#setSchemaTypeVisibilityForPackage}, or + * {@link SetSchemaRequest.Builder#setDocumentClassVisibilityForPackage} when building a schema. + * + *

Document access can also be granted to system UIs by specifying + * {@link SetSchemaRequest.Builder#setSchemaTypeDisplayedBySystem}, or + * {@link SetSchemaRequest.Builder#setDocumentClassDisplayedBySystem} + * when building a schema. + * + *

See {@link AppSearchSession#search} for a detailed explanation on + * forming a query string. + * + *

This method is lightweight. The heavy work will be done in + * {@link SearchResults#getNextPageAsync}. + * + * @param queryExpression query string to search. + * @param searchSpec spec for setting document filters, adding projection, setting term + * match type, etc. + * @return a {@link SearchResults} object for retrieved matched documents. + */ + @NonNull + SearchResults search(@NonNull String queryExpression, @NonNull SearchSpec searchSpec); + + /** + * Retrieves the collection of schemas most recently successfully provided to + * {@link AppSearchSession#setSchemaAsync} for any types belonging to the requested package and + * database that the caller has been granted access to. + * + *

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 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. + */ + // This call hits disk; async API prevents us from treating these calls as properties. + @SuppressLint("KotlinPropertyAccess") + @NonNull + ListenableFuture getSchemaAsync(@NonNull String packageName, + @NonNull String databaseName); + + /** + * Returns the {@link Features} to check for the availability of certain features + * for this session. + */ + @NonNull + Features getFeatures(); +}

diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
index 9fb1df0..c89fb1e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/FeatureConstants.java
@@ -27,13 +27,25 @@
  * @exportToFramework:hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-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}.  */
+    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/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index bfc4deb..e89dc99 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -16,7 +16,8 @@
 package androidx.appsearch.app;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
+
+import java.util.Set;
 
 /**
  * A class that encapsulates all features that are only supported in certain cases (e.g. only on
@@ -29,6 +30,8 @@
  */
 
 // @exportToFramework:copyToPath(../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external/Features.java)
+// Note: When adding new fields, The @RequiresFeature is needed in setters but could be skipped in
+// getters if call the getter won't send unsupported requests to the AppSearch-framework-impl.
 public interface Features {
 
     /**
@@ -60,8 +63,8 @@
 
     /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
-     * {@link SetSchemaRequest.Builder#addAllowedRoleForSchemaTypeVisibility},
-     * {@link SetSchemaRequest.Builder#clearAllowedRolesForSchemaTypeVisibility},
+     * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)},
+     * {@link SetSchemaRequest.Builder#clearRequiredPermissionsForSchemaTypeVisibility(String)},
      * {@link GetSchemaResponse#getSchemaTypesNotDisplayedBySystem()},
      * {@link GetSchemaResponse#getSchemaTypesVisibleToPackages()},
      * {@link GetSchemaResponse#getRequiredPermissionsForSchemaTypeVisibility()},
@@ -84,6 +87,7 @@
      * 

For details on the numeric search expressions in the query language, see * {@link AppSearchSession#search}. */ + // Note: The preferred name of this feature should have been LIST_FILTER_NUMERIC_SEARCH. String NUMERIC_SEARCH = FeatureConstants.NUMERIC_SEARCH; /** @@ -94,6 +98,7 @@ * *

For details on the verbatim string operator, see {@link AppSearchSession#search}. */ + // Note: The preferred name of this feature should have been LIST_FILTER_VERBATIM_SEARCH. String VERBATIM_SEARCH = FeatureConstants.VERBATIM_SEARCH; /** @@ -106,6 +111,42 @@ String LIST_FILTER_QUERY_LANGUAGE = FeatureConstants.LIST_FILTER_QUERY_LANGUAGE; /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the + * "hasProperty" function in query expressions. + * + *

For details on the "hasProperty" function in the query language, see + * {@link AppSearchSession#search}. + */ + String LIST_FILTER_HAS_PROPERTY_FUNCTION = FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers the use of the + * "tokenize" function in query expressions. + * + *

For details on the "tokenize" function in the query language, see + * {@link AppSearchSession#search}. + */ + String LIST_FILTER_TOKENIZE_FUNCTION = "LIST_FILTER_TOKENIZE_FUNCTION"; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers whether or not the + * AppSearch backend can store the descriptions returned by + * {@link AppSearchSchema#getDescription} and + * {@link AppSearchSchema.PropertyConfig#getDescription}. + */ + String SCHEMA_SET_DESCRIPTION = "SCHEMA_SET_DESCRIPTION"; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers + * {@link AppSearchSchema.EmbeddingPropertyConfig}. + * + *

For details on the embedding search expressions, see {@link AppSearchSession#search} for + * the query language and {@link SearchSpec.Builder#setRankingStrategy(String)} for the ranking + * language. + */ + String SCHEMA_EMBEDDING_PROPERTY_CONFIG = "SCHEMA_EMBEDDING_PROPERTY_CONFIG"; + + /** * Feature for {@link #isFeatureSupported(String)}. This feature covers * {@link SearchSpec#GROUPING_TYPE_PER_SCHEMA} */ @@ -119,10 +160,9 @@ /** * Feature for {@link #isFeatureSupported(String)}. This feature covers - * {@link SearchSpec.Builder#addFilterProperties}. - * @exportToFramework:hide + * {@link SearchSpec.Builder#addFilterProperties} and + * {@link SearchSuggestionSpec.Builder#addFilterProperties}. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) String SEARCH_SPEC_ADD_FILTER_PROPERTIES = "SEARCH_SPEC_ADD_FILTER_PROPERTIES"; /** @@ -145,12 +185,6 @@ String SEARCH_SUGGESTION = "SEARCH_SUGGESTION"; /** - * Feature for {@link #isFeatureSupported(String)}. This feature covers - * {@link AppSearchSchema.StringPropertyConfig.Builder#setDeletionPropagation}. - */ - String SCHEMA_SET_DELETION_PROPAGATION = "SCHEMA_SET_DELETION_PROPAGATION"; - - /** * Feature for {@link #isFeatureSupported(String)}. This feature covers setting schemas with * circular references for {@link AppSearchSession#setSchemaAsync}. */ @@ -170,6 +204,38 @@ String SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES = "SCHEMA_ADD_INDEXABLE_NESTED_PROPERTIES"; /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers + * {@link SearchSpec.Builder#setSearchSourceLogTag(String)}. + */ + String SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG = "SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG"; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers + * {@link SetSchemaRequest.Builder#setPubliclyVisibleSchema(String, PackageIdentifier)}. + */ + String SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE = "SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE"; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers + * {@link SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig}. + */ + String SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG = + "SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG"; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers + * {@link EnterpriseGlobalSearchSession} + */ + String ENTERPRISE_GLOBAL_SEARCH_SESSION = "ENTERPRISE_GLOBAL_SEARCH_SESSION"; + + /** + * Feature for {@link #isFeatureSupported(String)}. This feature covers + * {@link SearchSpec.Builder#addInformationalRankingExpressions}. + */ + String SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS = + "SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS"; + + /** * Returns whether a feature is supported at run-time. Feature support depends on the * feature in question, the AppSearch backend being used and the Android version of the * device.

diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 57134edba..6de5448 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -17,8 +17,6 @@
 package androidx.appsearch.app;
 
 import android.annotation.SuppressLint;
-import android.os.Bundle;
-import android.os.Parcelable;
 import android.util.Log;
 
 import androidx.annotation.IntRange;
@@ -27,9 +25,11 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
 import androidx.appsearch.annotation.Document;
-import androidx.appsearch.app.PropertyPath.PathSegment;
 import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.util.BundleUtil;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.GenericDocumentParcel;
+import androidx.appsearch.safeparcel.PropertyParcel;
 import androidx.appsearch.util.IndentingStringBuilder;
 import androidx.core.util.Preconditions;
 
@@ -39,6 +39,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -64,23 +65,9 @@
     /** The maximum number of indexed properties a document can have. */
     private static final int MAX_INDEXED_PROPERTIES = 16;
 
-    /** The default score of document. */
-    private static final int DEFAULT_SCORE = 0;
-
-    /** The default time-to-live in millisecond of a document, which is infinity. */
-    private static final long DEFAULT_TTL_MILLIS = 0L;
-
-    private static final String PROPERTIES_FIELD = "properties";
-    private static final String BYTE_ARRAY_FIELD = "byteArray";
-    private static final String SCHEMA_TYPE_FIELD = "schemaType";
-    private static final String ID_FIELD = "id";
-    private static final String SCORE_FIELD = "score";
-    private static final String TTL_MILLIS_FIELD = "ttlMillis";
-    private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
-    private static final String NAMESPACE_FIELD = "namespace";
-    private static final String PARENT_TYPES_FIELD = "parentTypes";
-
     /**
+     * Fixed constant synthetic property for parent types.
+     *
      * 
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -104,6 +91,7 @@
      * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE}.
      *
      * 
+     *
      * @deprecated This is no longer a static value, but depends on SDK version and what AppSearch
      * implementation is being used. Use {@link Features#getMaxIndexedProperties} instead.
      * 
@@ -138,43 +126,20 @@
     }
 // @exportToFramework:endStrip()
 
-    /**
-     * Contains all {@link GenericDocument} information in a packaged format.
-     *
-     * 

Keys are the {@code *_FIELD} constants in this class. - */ - @NonNull - final Bundle mBundle; - - /** Contains all properties in {@link GenericDocument} to support getting properties via name */ - @NonNull - private final Bundle mProperties; - - @NonNull - private final String mId; - @NonNull - private final String mSchemaType; - private final long mCreationTimestampMillis; - @Nullable - private Integer mHashCode; + /** The class to hold all meta data and properties for this {@link GenericDocument}. */ + private final GenericDocumentParcel mDocumentParcel; /** - * Rebuilds a {@link GenericDocument} from a bundle. + * Rebuilds a {@link GenericDocument} from a {@link GenericDocumentParcel}. * - * @param bundle Packaged {@link GenericDocument} data, such as the result of - * {@link #getBundle}. + * @param documentParcel Packaged {@link GenericDocument} data, such as the result of + * {@link #getDocumentParcel()}. * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @SuppressWarnings("deprecation") - public GenericDocument(@NonNull Bundle bundle) { - Preconditions.checkNotNull(bundle); - mBundle = bundle; - mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD)); - mId = Preconditions.checkNotNull(mBundle.getString(ID_FIELD)); - mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD)); - mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD, - System.currentTimeMillis()); + public GenericDocument(@NonNull GenericDocumentParcel documentParcel) { + mDocumentParcel = Objects.requireNonNull(documentParcel); } /** @@ -183,36 +148,37 @@ *

This method should be only used by constructor of a subclass. */ protected GenericDocument(@NonNull GenericDocument document) { - this(document.mBundle); + this(document.mDocumentParcel); } /** - * Returns the {@link Bundle} populated by this builder. + * Returns the {@link GenericDocumentParcel} holding the values for this + * {@link GenericDocument}. * * @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull - public Bundle getBundle() { - return mBundle; + public GenericDocumentParcel getDocumentParcel() { + return mDocumentParcel; } /** Returns the unique identifier of the {@link GenericDocument}. */ @NonNull public String getId() { - return mId; + return mDocumentParcel.getId(); } /** Returns the namespace of the {@link GenericDocument}. */ @NonNull public String getNamespace() { - return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/ ""); + return mDocumentParcel.getNamespace(); } /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */ @NonNull public String getSchemaType() { - return mSchemaType; + return mDocumentParcel.getSchemaType(); } /** @@ -224,7 +190,7 @@ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @Nullable public List getParentTypes() { - List result = mBundle.getStringArrayList(PARENT_TYPES_FIELD); + List result = mDocumentParcel.getParentTypes(); if (result == null) { return null; } @@ -238,7 +204,7 @@ */ /*@exportToFramework:CurrentTimeMillisLong*/ public long getCreationTimestampMillis() { - return mCreationTimestampMillis; + return mDocumentParcel.getCreationTimestampMillis(); } /** @@ -252,7 +218,7 @@ * until the app is uninstalled or {@link AppSearchSession#removeAsync} is called. */ public long getTtlMillis() { - return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS); + return mDocumentParcel.getTtlMillis(); } /** @@ -267,13 +233,13 @@ *

Any non-negative integer can be used a score. */ public int getScore() { - return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE); + return mDocumentParcel.getScore(); } /** Returns the names of all properties defined in this document. */ @NonNull public Set getPropertyNames() { - return Collections.unmodifiableSet(mProperties.keySet()); + return Collections.unmodifiableSet(mDocumentParcel.getPropertyNames()); } /** @@ -334,61 +300,36 @@ * * @param path The path to look for. * @return The entry with the given path as an object or {@code null} if there is no such path. - * The returned object will be one of the following types: {@code String[]}, {@code long[]}, - * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}. + * The returned object will be one of the following types: {@code String[]}, {@code long[]}, + * {@code double[]}, {@code boolean[]}, {@code byte[][]}, {@code GenericDocument[]}. */ @Nullable public Object getProperty(@NonNull String path) { - Preconditions.checkNotNull(path); + Objects.requireNonNull(path); Object rawValue = - getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/0, mBundle); + getRawPropertyFromRawDocument(new PropertyPath(path), /*pathIndex=*/ 0, + mDocumentParcel.getPropertyMap()); // Unpack the raw value into the types the user expects, if required. - if (rawValue instanceof Bundle) { - // getRawPropertyFromRawDocument may return a document as a bare Bundle as a performance - // optimization for lookups. - GenericDocument document = new GenericDocument((Bundle) rawValue); + if (rawValue instanceof GenericDocumentParcel) { + // getRawPropertyFromRawDocument may return a document as a bare documentParcel + // as a performance optimization for lookups. + GenericDocument document = new GenericDocument((GenericDocumentParcel) rawValue); return new GenericDocument[]{document}; } - if (rawValue instanceof List) { - // byte[][] fields are packed into List where each Bundle contains just a single - // entry: BYTE_ARRAY_FIELD -> byte[]. - @SuppressWarnings("unchecked") - List bundles = (List) rawValue; - byte[][] bytes = new byte[bundles.size()][]; - for (int i = 0; i < bundles.size(); i++) { - Bundle bundle = bundles.get(i); - if (bundle == null) { - Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path); - continue; - } - byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD); - if (innerBytes == null) { - Log.e(TAG, "The bundle at " + i + " contains a null byte[]."); - continue; - } - bytes[i] = innerBytes; - } - return bytes; - } - - if (rawValue instanceof Parcelable[]) { - // The underlying Bundle of nested GenericDocuments is packed into a Parcelable array. + if (rawValue instanceof GenericDocumentParcel[]) { + // The underlying parcelable of nested GenericDocuments is packed into + // a Parcelable array. // We must unpack it into GenericDocument instances. - Parcelable[] bundles = (Parcelable[]) rawValue; - GenericDocument[] documents = new GenericDocument[bundles.length]; - for (int i = 0; i < bundles.length; i++) { - if (bundles[i] == null) { - Log.e(TAG, "The inner bundle is null at " + i + ", for path: " + path); + GenericDocumentParcel[] docParcels = (GenericDocumentParcel[]) rawValue; + GenericDocument[] documents = new GenericDocument[docParcels.length]; + for (int i = 0; i < docParcels.length; i++) { + if (docParcels[i] == null) { + Log.e(TAG, "The inner parcel is null at " + i + ", for path: " + path); continue; } - if (!(bundles[i] instanceof Bundle)) { - Log.e(TAG, "The inner element at " + i + " is a " + bundles[i].getClass() - + ", not a Bundle for path: " + path); - continue; - } - documents[i] = new GenericDocument((Bundle) bundles[i]); + documents[i] = new GenericDocument(docParcels[i]); } return documents; } @@ -407,25 +348,21 @@ * But in the case where we collect documents across repeated nested documents, we need to * recurse back into this method, and so we also keep track of the index into the path. * - * @param path the PropertyPath object representing the path - * @param pathIndex the index into the path we start at - * @param documentBundle the bundle that contains the path we are looking up + * @param path the PropertyPath object representing the path + * @param pathIndex the index into the path we start at + * @param propertyMap the map containing the path we are looking up * @return the raw property */ @Nullable @SuppressWarnings("deprecation") private static Object getRawPropertyFromRawDocument( - @NonNull PropertyPath path, int pathIndex, @NonNull Bundle documentBundle) { - Preconditions.checkNotNull(path); - Preconditions.checkNotNull(documentBundle); - Bundle properties = Preconditions.checkNotNull(documentBundle.getBundle(PROPERTIES_FIELD)); - - + @NonNull PropertyPath path, int pathIndex, + @NonNull Map propertyMap) { + Objects.requireNonNull(path); + Objects.requireNonNull(propertyMap); for (int i = pathIndex; i < path.size(); i++) { - PathSegment segment = path.get(i); - - Object currentElementValue = properties.get(segment.getPropertyName()); - + PropertyPath.PathSegment segment = path.get(i); + Object currentElementValue = propertyMap.get(segment.getPropertyName()); if (currentElementValue == null) { return null; } @@ -435,61 +372,77 @@ // "recipients[0]", currentElementValue now contains the value of "recipients" while we // need the value of "recipients[0]". int index = segment.getPropertyIndex(); - if (index != PathSegment.NON_REPEATED_CARDINALITY) { + if (index != PropertyPath.PathSegment.NON_REPEATED_CARDINALITY) { + // For properties bundle, now we will only get PropertyParcel as the value. + PropertyParcel propertyParcel = (PropertyParcel) currentElementValue; + // Extract the right array element Object extractedValue = null; - if (currentElementValue instanceof String[]) { - String[] stringValues = (String[]) currentElementValue; - if (index < stringValues.length) { + if (propertyParcel.getStringValues() != null) { + String[] stringValues = propertyParcel.getStringValues(); + if (stringValues != null && index < stringValues.length) { extractedValue = Arrays.copyOfRange(stringValues, index, index + 1); } - } else if (currentElementValue instanceof long[]) { - long[] longValues = (long[]) currentElementValue; - if (index < longValues.length) { + } else if (propertyParcel.getLongValues() != null) { + long[] longValues = propertyParcel.getLongValues(); + if (longValues != null && index < longValues.length) { extractedValue = Arrays.copyOfRange(longValues, index, index + 1); } - } else if (currentElementValue instanceof double[]) { - double[] doubleValues = (double[]) currentElementValue; - if (index < doubleValues.length) { + } else if (propertyParcel.getDoubleValues() != null) { + double[] doubleValues = propertyParcel.getDoubleValues(); + if (doubleValues != null && index < doubleValues.length) { extractedValue = Arrays.copyOfRange(doubleValues, index, index + 1); } - } else if (currentElementValue instanceof boolean[]) { - boolean[] booleanValues = (boolean[]) currentElementValue; - if (index < booleanValues.length) { + } else if (propertyParcel.getBooleanValues() != null) { + boolean[] booleanValues = propertyParcel.getBooleanValues(); + if (booleanValues != null && index < booleanValues.length) { extractedValue = Arrays.copyOfRange(booleanValues, index, index + 1); } - } else if (currentElementValue instanceof List) { - @SuppressWarnings("unchecked") - List bundles = (List) currentElementValue; - if (index < bundles.size()) { - extractedValue = bundles.subList(index, index + 1); + } else if (propertyParcel.getBytesValues() != null) { + byte[][] bytesValues = propertyParcel.getBytesValues(); + if (bytesValues != null && index < bytesValues.length) { + extractedValue = Arrays.copyOfRange(bytesValues, index, index + 1); } - } else if (currentElementValue instanceof Parcelable[]) { + } else if (propertyParcel.getDocumentValues() != null) { // Special optimization: to avoid creating new singleton arrays for traversing - // paths we return the bare document Bundle in this particular case. - Parcelable[] bundles = (Parcelable[]) currentElementValue; - if (index < bundles.length) { - extractedValue = bundles[index]; + // paths we return the bare document parcel in this particular case. + GenericDocumentParcel[] docValues = propertyParcel.getDocumentValues(); + 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); + throw new IllegalStateException( + "Unsupported value type: " + currentElementValue); } currentElementValue = extractedValue; } // at the end of the path, either something like "...foo" or "...foo[1]" if (currentElementValue == null || i == path.size() - 1) { + if (currentElementValue != null && currentElementValue instanceof PropertyParcel) { + // Unlike previous bundle-based implementation, now each + // value is wrapped in PropertyParcel. + // Here we need to get and return the actual value for non-repeated fields. + currentElementValue = ((PropertyParcel) currentElementValue).getValues(); + } return currentElementValue; } - // currentElementValue is now a Bundle or Parcelable[], we can continue down the path - if (currentElementValue instanceof Bundle) { - properties = ((Bundle) currentElementValue).getBundle(PROPERTIES_FIELD); - } else if (currentElementValue instanceof Parcelable[]) { - Parcelable[] parcelables = (Parcelable[]) currentElementValue; - if (parcelables.length == 1) { - properties = ((Bundle) parcelables[0]).getBundle(PROPERTIES_FIELD); + // currentElementValue is now a GenericDocumentParcel or PropertyParcel, + // we can continue down the path. + if (currentElementValue instanceof GenericDocumentParcel) { + propertyMap = ((GenericDocumentParcel) currentElementValue).getPropertyMap(); + } else if (currentElementValue instanceof PropertyParcel + && ((PropertyParcel) currentElementValue).getDocumentValues() != null) { + GenericDocumentParcel[] docParcels = + ((PropertyParcel) currentElementValue).getDocumentValues(); + if (docParcels != null && docParcels.length == 1) { + propertyMap = docParcels[0].getPropertyMap(); continue; } @@ -516,17 +469,21 @@ // 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 accumulator = new ArrayList<>(parcelables.length); - for (Parcelable parcelable : parcelables) { - // recurse as we need to branch - Object value = getRawPropertyFromRawDocument(path, /*pathIndex=*/i + 1, - (Bundle) parcelable); - if (value != null) { - accumulator.add(value); + if (docParcels != null) { + List 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; @@ -540,10 +497,10 @@ * Combines accumulated repeated properties from multiple documents into a single array. * * @param accumulator List containing objects of the following types: {@code String[]}, - * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code List}, - * or {@code Parcelable[]}. + * {@code long[]}, {@code double[]}, {@code boolean[]}, {@code byte[][]}, + * or {@code GenericDocumentParcelable[]}. * @return The result of concatenating each individual list element into a larger array/list of - * the same type. + * the same type. */ @Nullable private static Object flattenAccumulator(@NonNull List accumulator) { @@ -607,28 +564,29 @@ } return result; } - if (first instanceof List) { + if (first instanceof byte[][]) { int length = 0; for (int i = 0; i < accumulator.size(); i++) { - length += ((List) accumulator.get(i)).size(); + length += ((byte[][]) accumulator.get(i)).length; } - List result = new ArrayList<>(length); + byte[][] result = new byte[length][]; + int total = 0; for (int i = 0; i < accumulator.size(); i++) { - @SuppressWarnings("unchecked") - List castValue = (List) accumulator.get(i); - result.addAll(castValue); + byte[][] castValue = (byte[][]) accumulator.get(i); + System.arraycopy(castValue, 0, result, total, castValue.length); + total += castValue.length; } return result; } - if (first instanceof Parcelable[]) { + if (first instanceof GenericDocumentParcel[]) { int length = 0; for (int i = 0; i < accumulator.size(); i++) { - length += ((Parcelable[]) accumulator.get(i)).length; + length += ((GenericDocumentParcel[]) accumulator.get(i)).length; } - Parcelable[] result = new Parcelable[length]; + GenericDocumentParcel[] result = new GenericDocumentParcel[length]; int total = 0; for (int i = 0; i < accumulator.size(); i++) { - Parcelable[] castValue = (Parcelable[]) accumulator.get(i); + GenericDocumentParcel[] castValue = (GenericDocumentParcel[]) accumulator.get(i); System.arraycopy(castValue, 0, result, total, castValue.length); total += castValue.length; } @@ -754,6 +712,27 @@ return propertyArray[0]; } + /** + * Retrieves an {@code EmbeddingVector} property by path. + * + *

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) { + Preconditions.checkNotNull(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) { @@ -862,13 +841,13 @@ * returns {@code null}. * * - *

If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]}, - * this method returns an empty {@code byte[][]}. + *

If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]}, + * this method returns an empty {@code byte[][]}. * * * @param path The path to look for. @@ -892,13 +871,13 @@ * returns {@code null}. * * - *

If it has been set via {@link Builder#setPropertyDocument} to an empty - * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}. + *

If it has been set via {@link Builder#setPropertyDocument} to an empty + * {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}. * * * @param path The path to look for. @@ -914,11 +893,36 @@ } /** + * Retrieves a repeated {@code EmbeddingVector[]} property by path. + * + *

See {@link #getProperty} for a detailed description of the path syntax. + * + *

If the property has not been set via {@link Builder#setPropertyEmbedding}, this method + * returns {@code null}. + * + *

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) { + Preconditions.checkNotNull(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. * - * @param path Path to the property within the document. Used for logging. - * @param value Value of the property + * @param path Path to the property within the document. Used for logging. + * @param value Value of the property * @param tClass Class to cast the value into */ @Nullable @@ -1056,6 +1060,7 @@ * {@link GenericDocument.Builder}. * *

The returned builder is a deep copy whose data is separate from this document. + * * @deprecated This API is not compliant with API guidelines. * Use {@link Builder#Builder(GenericDocument)} instead. * @@ -1064,8 +1069,7 @@ @NonNull @Deprecated public GenericDocument.Builder> toBuilder() { - Bundle clonedBundle = BundleUtil.deepCopy(mBundle); - return new GenericDocument.Builder<>(clonedBundle); + return new Builder<>(new GenericDocumentParcel.Builder(mDocumentParcel)); } @Override @@ -1077,15 +1081,12 @@ return false; } GenericDocument otherDocument = (GenericDocument) other; - return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle); + return mDocumentParcel.equals(otherDocument.mDocumentParcel); } @Override public int hashCode() { - if (mHashCode == null) { - mHashCode = BundleUtil.deepHashCode(mBundle); - } - return mHashCode; + return mDocumentParcel.hashCode(); } @Override @@ -1099,7 +1100,7 @@ /** * Appends a debug string for the {@link GenericDocument} instance to the given string builder. * - * @param builder the builder to append to. + * @param builder the builder to append to. */ void appendGenericDocumentString(@NonNull IndentingStringBuilder builder) { Preconditions.checkNotNull(builder); @@ -1147,9 +1148,9 @@ /** * Appends a debug string for the given document property to the given string builder. * - * @param propertyName name of property to create string for. - * @param property property object to create string for. - * @param builder the builder to append to. + * @param propertyName name of property to create string for. + * @param property property object to create string for. + * @param builder the builder to append to. */ private void appendPropertyString(@NonNull String propertyName, @NonNull Object property, @NonNull IndentingStringBuilder builder) { @@ -1178,7 +1179,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) { @@ -1197,11 +1198,10 @@ // This builder is specifically designed to be extended by classes deriving from // GenericDocument. @SuppressLint("StaticFinalBuilder") + @SuppressWarnings("rawtypes") public static class Builder { - private Bundle mBundle; - private Bundle mProperties; + private final GenericDocumentParcel.Builder mDocumentParcelBuilder; private final BuilderType mBuilderTypeInstance; - private boolean mBuilt = false; /** * Creates a new {@link GenericDocument.Builder}. @@ -1228,41 +1228,31 @@ Preconditions.checkNotNull(id); Preconditions.checkNotNull(schemaType); - mBundle = new Bundle(); mBuilderTypeInstance = (BuilderType) this; - mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace); - mBundle.putString(GenericDocument.ID_FIELD, id); - mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType); - mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS); - mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE); - - mProperties = new Bundle(); - mBundle.putBundle(PROPERTIES_FIELD, mProperties); + mDocumentParcelBuilder = new GenericDocumentParcel.Builder(namespace, id, schemaType); } /** - * Creates a new {@link GenericDocument.Builder} from the given Bundle. + * Creates a new {@link GenericDocument.Builder} from the given + * {@link GenericDocumentParcel.Builder}. * *

The bundle is NOT copied. */ @SuppressWarnings("unchecked") - Builder(@NonNull Bundle bundle) { - mBundle = Preconditions.checkNotNull(bundle); - // mProperties is NonNull and initialized to empty Bundle() in builder. - mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD)); + Builder(@NonNull GenericDocumentParcel.Builder documentParcelBuilder) { + mDocumentParcelBuilder = Objects.requireNonNull(documentParcelBuilder); mBuilderTypeInstance = (BuilderType) this; } /** * Creates a new {@link GenericDocument.Builder} from the given GenericDocument. * - *

The GenericDocument is deep copied, i.e. changes to the new GenericDocument - * returned by this function will NOT affect the original GenericDocument. - * + *

The GenericDocument is deep copied, that is, it changes to a new GenericDocument + * returned by this function and will NOT affect the original GenericDocument. */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_COPY_CONSTRUCTOR) public Builder(@NonNull GenericDocument document) { - this(BundleUtil.deepCopy(document.getBundle())); + this(new GenericDocumentParcel.Builder(document.mDocumentParcel)); } /** @@ -1272,14 +1262,13 @@ *

Document IDs are unique within a namespace. * *

The number of namespaces per app should be kept small for efficiency reasons. - * */ + @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType setNamespace(@NonNull String namespace) { Preconditions.checkNotNull(namespace); - resetIfBuilt(); - mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace); + mDocumentParcelBuilder.setNamespace(namespace); return mBuilderTypeInstance; } @@ -1287,15 +1276,17 @@ * Sets the ID of this document, changing the value provided in the constructor. No * special values are reserved or understood by the infrastructure. * - *

Document IDs are unique within a namespace. - * + *

Document IDs are unique within the combination of package, database, and namespace. + * + *

Setting a document with a duplicate id will overwrite the original document with + * the new document, enforcing uniqueness within the above constraint. */ + @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType setId(@NonNull String id) { Preconditions.checkNotNull(id); - resetIfBuilt(); - mBundle.putString(GenericDocument.ID_FIELD, id); + mDocumentParcelBuilder.setId(id); return mBuilderTypeInstance; } @@ -1303,15 +1294,15 @@ * Sets the schema type of this document, changing the value provided in the constructor. * *

To successfully index a document, the schema type must match the name of an - * {@link AppSearchSchema} object previously provided to {@link AppSearchSession#setSchemaAsync}. - * + * {@link AppSearchSchema} object previously provided to + * {@link AppSearchSession#setSchemaAsync}. */ + @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType setSchemaType(@NonNull String schemaType) { Preconditions.checkNotNull(schemaType); - resetIfBuilt(); - mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType); + mDocumentParcelBuilder.setSchemaType(schemaType); return mBuilderTypeInstance; } @@ -1326,9 +1317,7 @@ @NonNull public BuilderType setParentTypes(@NonNull List parentTypes) { Preconditions.checkNotNull(parentTypes); - resetIfBuilt(); - mBundle.putStringArrayList(GenericDocument.PARENT_TYPES_FIELD, - new ArrayList<>(parentTypes)); + mDocumentParcelBuilder.setParentTypes(parentTypes); return mBuilderTypeInstance; } @@ -1352,8 +1341,7 @@ if (score < 0) { throw new IllegalArgumentException("Document score cannot be negative."); } - resetIfBuilt(); - mBundle.putInt(GenericDocument.SCORE_FIELD, score); + mDocumentParcelBuilder.setScore(score); return mBuilderTypeInstance; } @@ -1371,9 +1359,7 @@ @NonNull public BuilderType setCreationTimestampMillis( /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) { - resetIfBuilt(); - mBundle.putLong( - GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, creationTimestampMillis); + mDocumentParcelBuilder.setCreationTimestampMillis(creationTimestampMillis); return mBuilderTypeInstance; } @@ -1397,8 +1383,7 @@ if (ttlMillis < 0) { throw new IllegalArgumentException("Document ttlMillis cannot be negative."); } - resetIfBuilt(); - mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis); + mDocumentParcelBuilder.setTtlMillis(ttlMillis); return mBuilderTypeInstance; } @@ -1406,9 +1391,9 @@ * Sets one or multiple {@code String} 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 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 String} values of the property. * @throws IllegalArgumentException if no values are provided, or if a passed in * {@code String} is {@code null} or "". @@ -1418,8 +1403,13 @@ public BuilderType setPropertyString(@NonNull String name, @NonNull String... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values); - resetIfBuilt(); - putInPropertyBundle(name, values); + validatePropertyName(name); + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("The String at " + i + " is null."); + } + } + mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; } @@ -1427,9 +1417,9 @@ * Sets one or multiple {@code boolean} 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 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 boolean} values of the property. * @throws IllegalArgumentException if the name is empty or {@code null}. */ @@ -1438,8 +1428,8 @@ public BuilderType setPropertyBoolean(@NonNull String name, @NonNull boolean... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values); - resetIfBuilt(); - putInPropertyBundle(name, values); + validatePropertyName(name); + mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; } @@ -1447,9 +1437,9 @@ * Sets one or multiple {@code long} 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 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 long} values of the property. * @throws IllegalArgumentException if the name is empty or {@code null}. */ @@ -1458,8 +1448,8 @@ public BuilderType setPropertyLong(@NonNull String name, @NonNull long... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values); - resetIfBuilt(); - putInPropertyBundle(name, values); + validatePropertyName(name); + mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; } @@ -1467,9 +1457,9 @@ * Sets one or multiple {@code double} 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 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 double} values of the property. * @throws IllegalArgumentException if the name is empty or {@code null}. */ @@ -1478,17 +1468,17 @@ public BuilderType setPropertyDouble(@NonNull String name, @NonNull double... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values); - resetIfBuilt(); - putInPropertyBundle(name, values); + validatePropertyName(name); + mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; } /** * Sets one or multiple {@code byte[]} 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 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 byte[]} of the property. * @throws IllegalArgumentException if no values are provided, or if a passed in * {@code byte[]} is {@code null}, or if name is empty. @@ -1498,8 +1488,13 @@ public BuilderType setPropertyBytes(@NonNull String name, @NonNull byte[]... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values); - resetIfBuilt(); - putInPropertyBundle(name, values); + validatePropertyName(name); + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("The byte[] at " + i + " is null."); + } + } + mDocumentParcelBuilder.putInPropertyMap(name, values); return mBuilderTypeInstance; } @@ -1507,9 +1502,9 @@ * Sets one or multiple {@link GenericDocument} 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 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 {@link GenericDocument} values of the property. * @throws IllegalArgumentException if no values are provided, or if a passed in * {@link GenericDocument} is {@code null}, or if name @@ -1521,8 +1516,43 @@ @NonNull String name, @NonNull GenericDocument... values) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(values); - resetIfBuilt(); - putInPropertyBundle(name, values); + validatePropertyName(name); + GenericDocumentParcel[] documentParcels = new GenericDocumentParcel[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + throw new IllegalArgumentException("The document at " + i + " is null."); + } + documentParcels[i] = values[i].getDocumentParcel(); + } + mDocumentParcelBuilder.putInPropertyMap(name, documentParcels); + return mBuilderTypeInstance; + } + + /** + * 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) { + Preconditions.checkNotNull(name); + Preconditions.checkNotNull(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; } @@ -1531,95 +1561,27 @@ * *

Note that this method does not support property paths. * + *

You should check for the existence of the property in {@link #getPropertyNames} if + * you need to make sure the property being cleared actually exists. + * + *

If the string passed is an invalid or nonexistent property, no error message or + * behavior will be observed. + * * @param name The name of the property to clear. - * */ + @FlaggedApi(Flags.FLAG_ENABLE_GENERIC_DOCUMENT_BUILDER_HIDDEN_METHODS) @CanIgnoreReturnValue @NonNull public BuilderType clearProperty(@NonNull String name) { Preconditions.checkNotNull(name); - resetIfBuilt(); - mProperties.remove(name); + mDocumentParcelBuilder.clearProperty(name); return mBuilderTypeInstance; } - private void putInPropertyBundle(@NonNull String name, @NonNull String[] values) - throws IllegalArgumentException { - validatePropertyName(name); - for (int i = 0; i < values.length; i++) { - if (values[i] == null) { - throw new IllegalArgumentException("The String at " + i + " is null."); - } - } - mProperties.putStringArray(name, values); - } - - private void putInPropertyBundle(@NonNull String name, @NonNull boolean[] values) { - validatePropertyName(name); - mProperties.putBooleanArray(name, values); - } - - private void putInPropertyBundle(@NonNull String name, @NonNull double[] values) { - validatePropertyName(name); - mProperties.putDoubleArray(name, values); - } - - private void putInPropertyBundle(@NonNull String name, @NonNull long[] values) { - validatePropertyName(name); - mProperties.putLongArray(name, values); - } - - /** - * Converts and saves a byte[][] into {@link #mProperties}. - * - *

Bundle doesn't support for two dimension array byte[][], we are converting byte[][] - * into ArrayList, and each elements will contain a one dimension byte[]. - */ - private void putInPropertyBundle(@NonNull String name, @NonNull byte[][] values) { - validatePropertyName(name); - ArrayList bundles = new ArrayList<>(values.length); - for (int i = 0; i < values.length; i++) { - if (values[i] == null) { - throw new IllegalArgumentException("The byte[] at " + i + " is null."); - } - Bundle bundle = new Bundle(); - bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]); - bundles.add(bundle); - } - mProperties.putParcelableArrayList(name, bundles); - } - - private void putInPropertyBundle(@NonNull String name, @NonNull GenericDocument[] values) { - validatePropertyName(name); - Parcelable[] documentBundles = new Parcelable[values.length]; - for (int i = 0; i < values.length; i++) { - if (values[i] == null) { - throw new IllegalArgumentException("The document at " + i + " is null."); - } - documentBundles[i] = values[i].mBundle; - } - mProperties.putParcelableArray(name, documentBundles); - } - /** Builds the {@link GenericDocument} object. */ @NonNull public GenericDocument build() { - mBuilt = true; - // Set current timestamp for creation timestamp by default. - if (mBundle.getLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, -1) == -1) { - mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD, - System.currentTimeMillis()); - } - return new GenericDocument(mBundle); - } - - private void resetIfBuilt() { - if (mBuilt) { - mBundle = BundleUtil.deepCopy(mBundle); - // mProperties is NonNull and initialized to empty Bundle() in builder. - mProperties = Preconditions.checkNotNull(mBundle.getBundle(PROPERTIES_FIELD)); - mBuilt = false; - } + return new GenericDocument(mDocumentParcelBuilder.build()); } /** Method to ensure property names are not blank */

diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
index ee59e34..be17430 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetByDocumentIdRequest.java
@@ -16,9 +16,20 @@
 
 package androidx.appsearch.app;
 
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GetByDocumentIdRequestCreator;
+import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -29,6 +40,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -37,7 +49,13 @@
  *
  * @see AppSearchSession#getByDocumentIdAsync
  */
-public final class GetByDocumentIdRequest {
+@SuppressWarnings("HiddenSuperclass")
[email protected](creator = "GetByDocumentIdRequestCreator")
+public final class GetByDocumentIdRequest extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull public static final Parcelable.Creator CREATOR =
+            new GetByDocumentIdRequestCreator();
     /**
      * Schema type to be used in
      * {@link GetByDocumentIdRequest.Builder#addProjection}
@@ -45,15 +63,30 @@
      * property paths set.
      */
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
-    private final String mNamespace;
-    private final Set mIds;
-    private final Map> mTypePropertyPathsMap;
 
-    GetByDocumentIdRequest(@NonNull String namespace, @NonNull Set ids, @NonNull Map
-            List> typePropertyPathsMap) {
-        mNamespace = Preconditions.checkNotNull(namespace);
-        mIds = Preconditions.checkNotNull(ids);
-        mTypePropertyPathsMap = Preconditions.checkNotNull(typePropertyPathsMap);
+    @NonNull
+    @Field(id = 1, getter = "getNamespace")
+    private final String mNamespace;
+    @NonNull
+    @Field(id = 2)
+    final List mIds;
+    @NonNull
+    @Field(id = 3)
+    final Bundle mTypePropertyPaths;
+
+    /**
+     * Cache of the ids. Comes from inflating mIds at first use.
+     */
+    @Nullable private Set mIdsCached;
+
+    @Constructor
+    GetByDocumentIdRequest(
+            @Param(id = 1) @NonNull String namespace,
+            @Param(id = 2) @NonNull List ids,
+            @Param(id = 3) @NonNull Bundle typePropertyPaths) {
+        mNamespace = Objects.requireNonNull(namespace);
+        mIds = Objects.requireNonNull(ids);
+        mTypePropertyPaths = Objects.requireNonNull(typePropertyPaths);
     }
 
     /** Returns the namespace attached to the request. */
@@ -65,7 +98,10 @@
     /** Returns the set of document IDs attached to the request. */
     @NonNull
     public Set getIds() {
-        return Collections.unmodifiableSet(mIds);
+        if (mIdsCached == null) {
+            mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
+        }
+        return mIdsCached;
     }
 
     /**
@@ -78,11 +114,15 @@
      */
     @NonNull
     public Map> getProjections() {
-        Map> copy = new ArrayMap<>();
-        for (Map.Entry> entry : mTypePropertyPathsMap.entrySet()) {
-            copy.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+        Set schemas = mTypePropertyPaths.keySet();
+        Map> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            List propertyPaths = mTypePropertyPaths.getStringArrayList(schema);
+            if (propertyPaths != null) {
+                typePropertyPathsMap.put(schema, Collections.unmodifiableList(propertyPaths));
+            }
         }
-        return copy;
+        return typePropertyPathsMap;
     }
 
     /**
@@ -95,38 +135,34 @@
      */
     @NonNull
     public Map> getProjectionPaths() {
-        Map> copy = new ArrayMap<>(mTypePropertyPathsMap.size());
-        for (Map.Entry> entry : mTypePropertyPathsMap.entrySet()) {
-            List propertyPathList = new ArrayList<>(entry.getValue().size());
-            for (String p: entry.getValue()) {
-                propertyPathList.add(new PropertyPath(p));
+        Set schemas = mTypePropertyPaths.keySet();
+        Map> typePropertyPathsMap = new ArrayMap<>(schemas.size());
+        for (String schema : schemas) {
+            List paths = mTypePropertyPaths.getStringArrayList(schema);
+            if (paths != null) {
+                int pathsSize = paths.size();
+                List 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.
-     *
-     * 

If the map is empty, then all properties will be retrieved for all results. - * - *

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. - * - * @exportToFramework:hide - */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @NonNull - public Map> 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 mIds = new ArraySet<>(); - private ArrayMap> mProjectionTypePropertyPaths = new ArrayMap<>(); + private List mIds = new ArrayList<>(); + private Bundle mProjectionTypePropertyPaths = new Bundle(); private boolean mBuilt = false; /** Creates a {@link GetByDocumentIdRequest.Builder} instance. */ @@ -176,12 +212,12 @@ Preconditions.checkNotNull(schemaType); Preconditions.checkNotNull(propertyPaths); resetIfBuilt(); - List propertyPathsList = new ArrayList<>(propertyPaths.size()); + ArrayList propertyPathsList = new ArrayList<>(propertyPaths.size()); for (String propertyPath : propertyPaths) { Preconditions.checkNotNull(propertyPath); propertyPathsList.add(propertyPath); } - mProjectionTypePropertyPaths.put(schemaType, propertyPathsList); + mProjectionTypePropertyPaths.putStringArrayList(schemaType, propertyPathsList); return this; } @@ -223,11 +259,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/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
index 0933654..74f370e 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GetSchemaResponse.java
@@ -17,7 +17,8 @@
 package androidx.appsearch.app;
 
 import android.annotation.SuppressLint;
-import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
@@ -25,6 +26,11 @@
 import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.CanIgnoreReturnValue;
+import androidx.appsearch.flags.FlaggedApi;
+import androidx.appsearch.flags.Flags;
+import androidx.appsearch.safeparcel.AbstractSafeParcelable;
+import androidx.appsearch.safeparcel.SafeParcelable;
+import androidx.appsearch.safeparcel.stub.StubCreators.GetSchemaResponseCreator;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -36,58 +42,88 @@
 import java.util.Set;
 
 /** The response class of {@link AppSearchSession#getSchemaAsync} */
-public final class GetSchemaResponse {
-    private static final String VERSION_FIELD = "version";
-    private static final String SCHEMAS_FIELD = "schemas";
-    private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =
-            "schemasNotDisplayedBySystem";
-    private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";
-    private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD =
-            "schemasVisibleToPermissions";
-    private static final String ALL_REQUIRED_PERMISSION_FIELD =
-            "allRequiredPermission";
[email protected](creator = "GetSchemaResponseCreator")
+@SuppressWarnings("HiddenSuperclass")
+public final class GetSchemaResponse extends AbstractSafeParcelable {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
+    @NonNull
+    public static final Parcelable.Creator CREATOR =
+            new GetSchemaResponseCreator();
+
+    @Field(id = 1, getter = "getVersion")
+    private final int mVersion;
+
+    @Field(id = 2)
+    final List mSchemas;
+
+    /**
+     * List of VisibilityConfigs for the current schema. May be {@code null} if retrieving the
+     * visibility settings is not possible on the current backend.
+     */
+    @Field(id = 3)
+    @Nullable
+    final List mVisibilityConfigs;
+
+    /**
+     * This set contains all schemas most recently successfully provided to
+     * {@link AppSearchSession#setSchemaAsync}. We do lazy fetch, the object will be created when
+     * you first time fetch it.
+     */
+    @Nullable
+    private Set mSchemasCached;
+
     /**
      * This Set contains all schemas that are not displayed by the system. All values in the set are
      * prefixed with the package-database prefix. We do lazy fetch, the object will be created
      * when you first time fetch it.
      */
     @Nullable
-    private Set mSchemasNotDisplayedBySystem;
+    private Set mSchemasNotDisplayedBySystemCached;
+
     /**
      * This map contains all schemas and {@link PackageIdentifier} that has access to 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.
      */
     @Nullable
-    private Map> mSchemasVisibleToPackages;
+    private Map> mSchemasVisibleToPackagesCached;
 
     /**
      * This map contains all schemas and Android Permissions combinations that are required to
      * 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
+     * objects under the {@code schemaType} if they hold ALL required permissions of ANY
      * combinations.
-     * The value set represents
-     * {@link androidx.appsearch.app.SetSchemaRequest.AppSearchSupportedPermission}.
+     * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility(String, Set)
      */
     @Nullable
-    private Map>> mSchemasVisibleToPermissions;
-
-    private final Bundle mBundle;
-
-    GetSchemaResponse(@NonNull Bundle bundle) {
-        mBundle = Preconditions.checkNotNull(bundle);
-    }
+    private Map>> mSchemasVisibleToPermissionsCached;
 
     /**
-     * Returns the {@link Bundle} populated by this builder.
-     * @exportToFramework:hide
+     * This map contains all publicly visible schemas and the {@link PackageIdentifier} specifying
+     * the package that the schemas are from.
      */
-    @NonNull
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public Bundle getBundle() {
-        return mBundle;
+    @Nullable
+    private Map mPubliclyVisibleSchemasCached;
+
+    /**
+     * This map contains all {@link SchemaVisibilityConfig}s that has access to 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.
+     */
+    @Nullable
+    private Map> mSchemasVisibleToConfigsCached;
+
+    @Constructor
+    GetSchemaResponse(
+            @Param(id = 1) int version,
+            @Param(id = 2) @NonNull List schemas,
+            @Param(id = 3) @Nullable List visibilityConfigs) {
+        mVersion = version;
+        mSchemas = Preconditions.checkNotNull(schemas);
+        mVisibilityConfigs = visibilityConfigs;
     }
 
     /**
@@ -97,25 +133,19 @@
      */
     @IntRange(from = 0)
     public int getVersion() {
-        return mBundle.getInt(VERSION_FIELD);
+        return mVersion;
     }
 
     /**
      * Return the schemas most recently successfully provided to
      * {@link AppSearchSession#setSchemaAsync}.
-     *
-     * 

It is inefficient to call this method repeatedly. */ @NonNull - @SuppressWarnings("deprecation") public Set getSchemas() { - ArrayList schemaBundles = Preconditions.checkNotNull( - mBundle.getParcelableArrayList(SCHEMAS_FIELD)); - Set schemas = new ArraySet<>(schemaBundles.size()); - for (int i = 0; i < schemaBundles.size(); i++) { - schemas.add(new AppSearchSchema(schemaBundles.get(i))); + if (mSchemasCached == null) { + mSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mSchemas)); } - return schemas; + return mSchemasCached; } /** @@ -126,21 +156,22 @@ * called with false. * */ - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) - // @exportToFramework:endStrip() @NonNull public Set getSchemaTypesNotDisplayedBySystem() { - checkGetVisibilitySettingSupported(); - if (mSchemasNotDisplayedBySystem == null) { - List schemasNotDisplayedBySystemList = - mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD); - mSchemasNotDisplayedBySystem = - Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList)); + List visibilityConfigs = getVisibilityConfigsOrThrow(); + if (mSchemasNotDisplayedBySystemCached == null) { + Set copy = new ArraySet<>(); + for (int i = 0; i < visibilityConfigs.size(); i++) { + if (visibilityConfigs.get(i).isNotDisplayedBySystem()) { + copy.add(visibilityConfigs.get(i).getSchemaType()); + } + } + mSchemasNotDisplayedBySystemCached = Collections.unmodifiableSet(copy); } - return mSchemasNotDisplayedBySystem; + return mSchemasNotDisplayedBySystemCached; } /** @@ -151,37 +182,32 @@ * called with false. * */ - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) - // @exportToFramework:endStrip() @NonNull - @SuppressWarnings("deprecation") public Map> getSchemaTypesVisibleToPackages() { - checkGetVisibilitySettingSupported(); - if (mSchemasVisibleToPackages == null) { - Bundle schemaVisibleToPackagesBundle = Preconditions.checkNotNull( - mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)); + List visibilityConfigs = getVisibilityConfigsOrThrow(); + if (mSchemasVisibleToPackagesCached == null) { Map> copy = new ArrayMap<>(); - for (String key : schemaVisibleToPackagesBundle.keySet()) { - List PackageIdentifierBundles = Preconditions.checkNotNull( - schemaVisibleToPackagesBundle.getParcelableArrayList(key)); - Set packageIdentifiers = - new ArraySet<>(PackageIdentifierBundles.size()); - for (int i = 0; i < PackageIdentifierBundles.size(); i++) { - packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i))); + for (int i = 0; i < visibilityConfigs.size(); i++) { + InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); + List visibleToPackages = + visibilityConfig.getVisibilityConfig().getAllowedPackages(); + if (!visibleToPackages.isEmpty()) { + copy.put( + visibilityConfig.getSchemaType(), + Collections.unmodifiableSet(new ArraySet<>(visibleToPackages))); } - copy.put(key, packageIdentifiers); } - mSchemasVisibleToPackages = Collections.unmodifiableMap(copy); + mSchemasVisibleToPackagesCached = Collections.unmodifiableMap(copy); } - return mSchemasVisibleToPackages; + return mSchemasVisibleToPackagesCached; } /** - * Returns a mapping of schema types to the Map of {@link android.Manifest.permission} - * combinations that querier must hold to access that schema type. + * Returns a mapping of schema types to the set of {@link android.Manifest.permission} + * combination sets that querier must hold to access that schema type. * *

The querier could read the {@link GenericDocument} objects under the {@code schemaType} * if they holds ALL required permissions of ANY of the individual value sets. @@ -189,12 +215,12 @@ *

For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB}, * { PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}. *

    - *
  • A querier holds both PermissionA and PermissionB has access.
  • - *
  • A querier holds both PermissionC and PermissionD has access.
  • - *
  • A querier holds only PermissionE has access.
  • - *
  • A querier holds both PermissionA and PermissionE has access.
  • - *
  • A querier holds only PermissionA doesn't have access.
  • - *
  • A querier holds both PermissionA and PermissionC doesn't have access.
  • + *
  • A querier holding both PermissionA and PermissionB has access.
  • + *
  • A querier holding both PermissionC and PermissionD has access.
  • + *
  • A querier holding only PermissionE has access.
  • + *
  • A querier holding both PermissionA and PermissionE has access.
  • + *
  • A querier holding only PermissionA doesn't have access.
  • + *
  • A querier holding only PermissionA and PermissionC doesn't have access.
  • *
* * @return The map contains schema type and all combinations of required permission for querier @@ -208,56 +234,118 @@ * called with false. * */ - // @exportToFramework:startStrip() + // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) - // @exportToFramework:endStrip() @NonNull - @SuppressWarnings("deprecation") public Map>> getRequiredPermissionsForSchemaTypeVisibility() { - checkGetVisibilitySettingSupported(); - if (mSchemasVisibleToPermissions == null) { + List visibilityConfigs = getVisibilityConfigsOrThrow(); + if (mSchemasVisibleToPermissionsCached == null) { Map>> copy = new ArrayMap<>(); - Bundle schemaVisibleToPermissionBundle = Preconditions.checkNotNull( - mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD)); - for (String key : schemaVisibleToPermissionBundle.keySet()) { - ArrayList allRequiredPermissionsBundle = - schemaVisibleToPermissionBundle.getParcelableArrayList(key); - Set> visibleToPermissions = new ArraySet<>(); - if (allRequiredPermissionsBundle != null) { - // This should never be null - for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) { - visibleToPermissions.add(new ArraySet<>(allRequiredPermissionsBundle.get(i) - .getIntegerArrayList(ALL_REQUIRED_PERMISSION_FIELD))); - } + for (int i = 0; i < visibilityConfigs.size(); i++) { + InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); + Set> visibleToPermissions = + visibilityConfig.getVisibilityConfig().getRequiredPermissions(); + if (!visibleToPermissions.isEmpty()) { + copy.put( + visibilityConfig.getSchemaType(), + Collections.unmodifiableSet(visibleToPermissions)); } - copy.put(key, visibleToPermissions); } - mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy); + mSchemasVisibleToPermissionsCached = Collections.unmodifiableMap(copy); } - return mSchemasVisibleToPermissions; + return mSchemasVisibleToPermissionsCached; } - private void checkGetVisibilitySettingSupported() { - if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) { + /** + * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying + * the package the schemas are from. + * + *

If no schemas have been set as publicly visible, an empty set will be returned. + * + * @throws UnsupportedOperationException if {@link Builder#setVisibilitySettingSupported} was + * called with false. + * + */ + @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA) + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) + @NonNull + public Map getPubliclyVisibleSchemas() { + List visibilityConfigs = getVisibilityConfigsOrThrow(); + if (mPubliclyVisibleSchemasCached == null) { + Map copy = new ArrayMap<>(); + for (int i = 0; i < visibilityConfigs.size(); i++) { + InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); + PackageIdentifier publiclyVisibleTargetPackage = + visibilityConfig.getVisibilityConfig().getPubliclyVisibleTargetPackage(); + if (publiclyVisibleTargetPackage != null) { + copy.put(visibilityConfig.getSchemaType(), publiclyVisibleTargetPackage); + } + } + mPubliclyVisibleSchemasCached = Collections.unmodifiableMap(copy); + } + return mPubliclyVisibleSchemasCached; + } + + /** + * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have + * access to that schema type. + * + * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig + */ + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) + @NonNull + public Map> getSchemaTypesVisibleToConfigs() { + List visibilityConfigs = getVisibilityConfigsOrThrow(); + if (mSchemasVisibleToConfigsCached == null) { + Map> copy = new ArrayMap<>(); + for (int i = 0; i < visibilityConfigs.size(); i++) { + InternalVisibilityConfig visibilityConfig = visibilityConfigs.get(i); + Set nestedVisibilityConfigs = + visibilityConfig.getVisibleToConfigs(); + if (!nestedVisibilityConfigs.isEmpty()) { + copy.put(visibilityConfig.getSchemaType(), + Collections.unmodifiableSet(nestedVisibilityConfigs)); + } + } + mSchemasVisibleToConfigsCached = Collections.unmodifiableMap(copy); + } + return mSchemasVisibleToConfigsCached; + } + + @NonNull + private List getVisibilityConfigsOrThrow() { + List visibilityConfigs = mVisibilityConfigs; + if (visibilityConfigs == null) { throw new UnsupportedOperationException("Get visibility setting is not supported with " + "this backend/Android API level combination."); } + return visibilityConfigs; + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + GetSchemaResponseCreator.writeToParcel(this, dest, flags); } /** Builder for {@link GetSchemaResponse} objects. */ public static final class Builder { private int mVersion = 0; - private ArrayList mSchemaBundles = new ArrayList<>(); + private ArrayList 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. */ @Nullable - private ArrayList mSchemasNotDisplayedBySystem; - private Bundle mSchemasVisibleToPackages; - private Bundle mSchemasVisibleToPermissions; + private Map mVisibilityConfigBuilders; private boolean mBuilt = false; /** Create a {@link Builder} object} */ @@ -284,7 +372,7 @@ public Builder addSchema(@NonNull AppSearchSchema schema) { Preconditions.checkNotNull(schema); resetIfBuilt(); - mSchemaBundles.add(schema.getBundle()); + mSchemas.add(schema); return this; } @@ -302,10 +390,9 @@ public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) { Preconditions.checkNotNull(schemaType); resetIfBuilt(); - if (mSchemasNotDisplayedBySystem == null) { - mSchemasNotDisplayedBySystem = new ArrayList<>(); - } - mSchemasNotDisplayedBySystem.add(schemaType); + InternalVisibilityConfig.Builder visibilityConfigBuilder = + getOrCreateVisibilityConfigBuilder(schemaType); + visibilityConfigBuilder.setNotDisplayedBySystem(true); return this; } @@ -337,11 +424,11 @@ Preconditions.checkNotNull(schemaType); Preconditions.checkNotNull(packageIdentifiers); resetIfBuilt(); - ArrayList bundles = new ArrayList<>(packageIdentifiers.size()); + InternalVisibilityConfig.Builder visibilityConfigBuilder = + getOrCreateVisibilityConfigBuilder(schemaType); for (PackageIdentifier packageIdentifier : packageIdentifiers) { - bundles.add(packageIdentifier.getBundle()); + visibilityConfigBuilder.addVisibleToPackage(packageIdentifier); } - mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles); return this; } @@ -364,41 +451,103 @@ *

  • A querier holds both PermissionA and PermissionC doesn't have access.
  • * * + * @param schemaType The schema type to set visibility on. + * @param visibleToPermissionSets The Sets of Android permissions that will be required to + * access the given schema. * @see android.Manifest.permission#READ_SMS * @see android.Manifest.permission#READ_CALENDAR * @see android.Manifest.permission#READ_CONTACTS * @see android.Manifest.permission#READ_EXTERNAL_STORAGE * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA - * - * @param schemaType The schema type to set visibility on. - * @param visibleToPermissions The Android permissions that will be required to access - * the given schema. */ + // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes. @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") + // @SetSchemaRequest is an IntDef annotation applied to Set>. + @SuppressWarnings("SupportAnnotationUsage") @NonNull public Builder setRequiredPermissionsForSchemaTypeVisibility( @NonNull String schemaType, @SetSchemaRequest.AppSearchSupportedPermission @NonNull - Set> visibleToPermissions) { + Set> visibleToPermissionSets) { Preconditions.checkNotNull(schemaType); - Preconditions.checkNotNull(visibleToPermissions); + Preconditions.checkNotNull(visibleToPermissionSets); resetIfBuilt(); - ArrayList visibleToPermissionsBundle = new ArrayList<>(); - for (Set allRequiredPermissions : visibleToPermissions) { - for (int permission : allRequiredPermissions) { - Preconditions.checkArgumentInRange(permission, SetSchemaRequest.READ_SMS, - SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA, "permission"); - } - Bundle allRequiredPermissionsBundle = new Bundle(); - allRequiredPermissionsBundle.putIntegerArrayList( - ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions)); - visibleToPermissionsBundle.add(allRequiredPermissionsBundle); + InternalVisibilityConfig.Builder visibilityConfigBuilder = + getOrCreateVisibilityConfigBuilder(schemaType); + for (Set visibleToPermissions : visibleToPermissionSets) { + visibilityConfigBuilder.addVisibleToPermissions(visibleToPermissions); } - mSchemasVisibleToPermissions.putParcelableArrayList(schemaType, - visibleToPermissionsBundle); + return this; + } + + /** + * Specify that the schema should be publicly available, to packages which already have + * visibility to {@code packageIdentifier}. + * + * @param schemaType the schema to make publicly accessible. + * @param packageIdentifier the package from which the document schema is from. + * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema + */ + // Merged list available from getPubliclyVisibleSchemas + @CanIgnoreReturnValue + @SuppressLint("MissingGetterMatchingBuilder") + @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA) + @NonNull + public Builder setPubliclyVisibleSchema( + @NonNull String schemaType, @NonNull PackageIdentifier packageIdentifier) { + Preconditions.checkNotNull(schemaType); + Preconditions.checkNotNull(packageIdentifier); + resetIfBuilt(); + InternalVisibilityConfig.Builder visibilityConfigBuilder = + getOrCreateVisibilityConfigBuilder(schemaType); + visibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier); + return this; + } + + /** + * Sets the documents from the provided {@code schemaType} can be read by the caller if they + * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}. + * + *

    The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A + * caller must match ALL requirements to access the schema. For example, a caller must hold + * required permissions AND it is a specified package. + * + *

    The querier could have access if they match ALL requirements in ANY of the given + * {@link SchemaVisibilityConfig}s + * + *

    For example, if the Set contains {@code {% verbatim %}{{PackageA and Permission1}, + * {PackageB and Permission2}}{% endverbatim %}}. + *

      + *
    • A querier from packageA could read if they holds Permission1.
    • + *
    • A querier from packageA could NOT read if they only holds Permission2 instead of + * Permission1.
    • + *
    • A querier from packageB could read if they holds Permission2.
    • + *
    • A querier from packageC could never read.
    • + *
    • A querier holds both PermissionA and PermissionE has access.
    • + *
    + * + * @param schemaType The schema type to set visibility on. + * @param visibleToConfigs The {@link SchemaVisibilityConfig}s hold all requirements that + * a 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 + public Builder setSchemaTypeVisibleToConfigs(@NonNull String schemaType, + @NonNull Set visibleToConfigs) { + Preconditions.checkNotNull(schemaType); + Preconditions.checkNotNull(visibleToConfigs); + resetIfBuilt(); + InternalVisibilityConfig.Builder visibilityConfigBuilder = + getOrCreateVisibilityConfigBuilder(schemaType); + for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) { + visibilityConfigBuilder.addVisibleToConfig(visibleToConfig); + } return this; } @@ -416,17 +565,14 @@ * @exportToFramework:hide */ // Visibility setting is determined by SDK version, so it won't be needed in framework + @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull public Builder setVisibilitySettingSupported(boolean visibilitySettingSupported) { if (visibilitySettingSupported) { - mSchemasNotDisplayedBySystem = new ArrayList<>(); - mSchemasVisibleToPackages = new Bundle(); - mSchemasVisibleToPermissions = new Bundle(); + mVisibilityConfigBuilders = new ArrayMap<>(); } else { - mSchemasNotDisplayedBySystem = null; - mSchemasVisibleToPackages = null; - mSchemasVisibleToPermissions = null; + mVisibilityConfigBuilders = null; } return this; } @@ -434,33 +580,37 @@ /** Builds a {@link GetSchemaResponse} object. */ @NonNull public GetSchemaResponse build() { - Bundle bundle = new Bundle(); - bundle.putInt(VERSION_FIELD, mVersion); - bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles); - if (mSchemasNotDisplayedBySystem != null) { - // Only save the visibility fields if it was actually set. - bundle.putStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD, - mSchemasNotDisplayedBySystem); - bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages); - bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions); + List visibilityConfigs = null; + if (mVisibilityConfigBuilders != null) { + visibilityConfigs = new ArrayList<>(); + for (InternalVisibilityConfig.Builder builder : + mVisibilityConfigBuilders.values()) { + visibilityConfigs.add(builder.build()); + } } mBuilt = true; - return new GetSchemaResponse(bundle); + return new GetSchemaResponse(mVersion, mSchemas, visibilityConfigs); + } + + @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); + mVisibilityConfigBuilders.put(schemaType, builder); + } + return builder; } private void resetIfBuilt() { if (mBuilt) { - mSchemaBundles = new ArrayList<>(mSchemaBundles); - if (mSchemasNotDisplayedBySystem != null) { - // Only reset the visibility fields if it was actually set. - mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem); - Bundle copyVisibleToPackages = new Bundle(); - copyVisibleToPackages.putAll(mSchemasVisibleToPackages); - mSchemasVisibleToPackages = copyVisibleToPackages; - Bundle copyVisibleToPermissions = new Bundle(); - copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions); - mSchemasVisibleToPermissions = copyVisibleToPermissions; - } + // No need to copy mVisibilityConfigBuilders -- it gets copied during build(). + mSchemas = new ArrayList<>(mSchemas); mBuilt = false; } }
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
    index 7f9dfb0..17fbfbd 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
    
    @@ -53,11 +53,9 @@
          * @param request a request containing a namespace and IDs of the documents to retrieve.
          */
         @NonNull
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.GLOBAL_SEARCH_SESSION_GET_BY_ID)
    -    // @exportToFramework:endStrip()
         ListenableFuture> getByDocumentIdAsync(
                 @NonNull String packageName,
                 @NonNull String databaseName,
    @@ -127,11 +125,9 @@
         // This call hits disk; async API prevents us from treating these calls as properties.
         @SuppressLint("KotlinPropertyAccess")
         @NonNull
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA)
    -    // @exportToFramework:endStrip()
         ListenableFuture getSchemaAsync(@NonNull String packageName,
                 @NonNull String databaseName);
     
    @@ -170,11 +166,9 @@
          * @throws UnsupportedOperationException if this feature is not available on this
          *                                       AppSearch implementation.
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)
    -    // @exportToFramework:endStrip()
         void registerObserverCallback(
                 @NonNull String targetPackageName,
                 @NonNull ObserverSpec spec,
    @@ -204,11 +198,9 @@
          * @throws UnsupportedOperationException if this feature is not available on this
          *                                       AppSearch implementation.
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK)
    -    // @exportToFramework:endStrip()
         void unregisterObserverCallback(
                 @NonNull String targetPackageName, @NonNull ObserverCallback observer)
                 throws AppSearchException;
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
    index 3a1da37..2ece210 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalSetSchemaResponse.java
    
    @@ -16,11 +16,17 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.InternalSetSchemaResponseCreator;
     import androidx.core.util.Preconditions;
     
     /**
    @@ -34,36 +40,30 @@
      * @exportToFramework:hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -public class InternalSetSchemaResponse {
    -
    -    private static final String IS_SUCCESS_FIELD = "isSuccess";
    -    private static final String SET_SCHEMA_RESPONSE_BUNDLE_FIELD = "setSchemaResponseBundle";
    -    private static final String ERROR_MESSAGE_FIELD = "errorMessage";
    -
    -    private final Bundle mBundle;
    -
    -    public InternalSetSchemaResponse(@NonNull Bundle bundle) {
    -        mBundle = Preconditions.checkNotNull(bundle);
    -    }
    -
    -    private InternalSetSchemaResponse(boolean isSuccess,
    -            @NonNull SetSchemaResponse setSchemaResponse,
    -            @Nullable String errorMessage) {
    -        Preconditions.checkNotNull(setSchemaResponse);
    -        mBundle = new Bundle();
    -        mBundle.putBoolean(IS_SUCCESS_FIELD, isSuccess);
    -        mBundle.putBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD, setSchemaResponse.getBundle());
    -        mBundle.putString(ERROR_MESSAGE_FIELD, errorMessage);
    -    }
    -
    -    /**
    -     * Returns the {@link Bundle} populated by this builder.
    -     * @exportToFramework:hide
    -     */
    -    @NonNull
    [email protected](creator = "InternalSetSchemaResponseCreator")
    +public class InternalSetSchemaResponse extends AbstractSafeParcelable {
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public Bundle getBundle() {
    -        return mBundle;
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull public static final Parcelable.Creator CREATOR =
    +            new InternalSetSchemaResponseCreator();
    +
    +    @Field(id = 1, getter = "isSuccess")
    +    private final boolean mIsSuccess;
    +
    +    @Field(id = 2, getter = "getSetSchemaResponse")
    +    private final SetSchemaResponse mSetSchemaResponse;
    +    @Field(id = 3, getter = "getErrorMessage")
    +    @Nullable private final String mErrorMessage;
    +
    +    @Constructor
    +    public InternalSetSchemaResponse(
    +            @Param(id = 1) boolean isSuccess,
    +            @Param(id = 2) @NonNull SetSchemaResponse setSchemaResponse,
    +            @Param(id = 3) @Nullable String errorMessage) {
    +        Preconditions.checkNotNull(setSchemaResponse);
    +        mIsSuccess = isSuccess;
    +        mSetSchemaResponse = setSchemaResponse;
    +        mErrorMessage = errorMessage;
         }
     
         /**
    @@ -94,7 +94,7 @@
     
         /** Returns {@code true} if the schema request is proceeded successfully. */
         public boolean isSuccess() {
    -        return mBundle.getBoolean(IS_SUCCESS_FIELD);
    +        return mIsSuccess;
         }
     
         /**
    @@ -104,7 +104,7 @@
          */
         @NonNull
         public SetSchemaResponse getSetSchemaResponse() {
    -        return new SetSchemaResponse(mBundle.getBundle(SET_SCHEMA_RESPONSE_BUNDLE_FIELD));
    +        return mSetSchemaResponse;
         }
     
     
    @@ -115,6 +115,13 @@
          */
         @Nullable
         public String getErrorMessage() {
    -        return mBundle.getString(ERROR_MESSAGE_FIELD);
    +        return mErrorMessage;
    +    }
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        InternalSetSchemaResponseCreator.writeToParcel(this, dest, flags);
         }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java
    new file mode 100644
    index 0000000..843d1cf
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/InternalVisibilityConfig.java
    
    @@ -0,0 +1,382 @@
    +/*
    + * 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 androidx.appsearch.app;
    +
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.InternalVisibilityConfigCreator;
    +import androidx.collection.ArraySet;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Objects;
    +import java.util.Set;
    +
    +/**
    + * An expanded version of {@link SchemaVisibilityConfig} which includes fields for internal use by
    + * AppSearch.
    + *
    + * @exportToFramework:hide
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    [email protected](creator = "InternalVisibilityConfigCreator")
    +public final class InternalVisibilityConfig extends AbstractSafeParcelable {
    +    @NonNull
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    public static final Parcelable.Creator CREATOR =
    +            new InternalVisibilityConfigCreator();
    +
    +    /**
    +     * Build the List of {@link InternalVisibilityConfig}s from visibility settings.
    +     */
    +    @NonNull
    +    public static List toInternalVisibilityConfigs(
    +            @NonNull SetSchemaRequest setSchemaRequest) {
    +        Set searchSchemas = setSchemaRequest.getSchemas();
    +        Set schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem();
    +        Map> schemasVisibleToPackages =
    +                setSchemaRequest.getSchemasVisibleToPackages();
    +        Map>> schemasVisibleToPermissions =
    +                setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility();
    +        Map publiclyVisibleSchemas =
    +                setSchemaRequest.getPubliclyVisibleSchemas();
    +        Map> schemasVisibleToConfigs =
    +                setSchemaRequest.getSchemasVisibleToConfigs();
    +
    +        List result = new ArrayList<>(searchSchemas.size());
    +        for (AppSearchSchema searchSchema : searchSchemas) {
    +            String schemaType = searchSchema.getSchemaType();
    +            InternalVisibilityConfig.Builder builder =
    +                    new InternalVisibilityConfig.Builder(schemaType)
    +                            .setNotDisplayedBySystem(
    +                                    schemasNotDisplayedBySystem.contains(schemaType));
    +
    +            Set visibleToPackages = schemasVisibleToPackages.get(schemaType);
    +            if (visibleToPackages != null) {
    +                for (PackageIdentifier packageIdentifier : visibleToPackages) {
    +                    builder.addVisibleToPackage(packageIdentifier);
    +                }
    +            }
    +
    +            Set> visibleToPermissionSets = schemasVisibleToPermissions.get(schemaType);
    +            if (visibleToPermissionSets != null) {
    +                for (Set visibleToPermissions : visibleToPermissionSets) {
    +                    builder.addVisibleToPermissions(visibleToPermissions);
    +                }
    +            }
    +
    +            PackageIdentifier publiclyVisibleTargetPackage = publiclyVisibleSchemas.get(schemaType);
    +            if (publiclyVisibleTargetPackage != null) {
    +                builder.setPubliclyVisibleTargetPackage(publiclyVisibleTargetPackage);
    +            }
    +
    +            Set visibleToConfigs = schemasVisibleToConfigs.get(schemaType);
    +            if (visibleToConfigs != null) {
    +                for (SchemaVisibilityConfig schemaVisibilityConfig : visibleToConfigs) {
    +                    builder.addVisibleToConfig(schemaVisibilityConfig);
    +                }
    +            }
    +
    +            result.add(builder.build());
    +        }
    +        return result;
    +    }
    +
    +    @NonNull
    +    @Field(id = 1, getter = "getSchemaType")
    +    private final String mSchemaType;
    +
    +    @Field(id = 2, getter = "isNotDisplayedBySystem")
    +    private final boolean mIsNotDisplayedBySystem;
    +
    +    /** The public visibility settings available in VisibilityConfig. */
    +    @NonNull
    +    @Field(id = 3, getter = "getVisibilityConfig")
    +    private final SchemaVisibilityConfig mVisibilityConfig;
    +
    +    /** Extended visibility settings from {@link SetSchemaRequest#getSchemasVisibleToConfigs()} */
    +    @NonNull
    +    @Field(id = 4)
    +    final List mVisibleToConfigs;
    +
    +    @Constructor
    +    InternalVisibilityConfig(
    +            @Param(id = 1) @NonNull String schemaType,
    +            @Param(id = 2) boolean isNotDisplayedBySystem,
    +            @Param(id = 3) @NonNull SchemaVisibilityConfig schemaVisibilityConfig,
    +            @Param(id = 4) @NonNull List visibleToConfigs) {
    +        mIsNotDisplayedBySystem = isNotDisplayedBySystem;
    +        mSchemaType = Objects.requireNonNull(schemaType);
    +        mVisibilityConfig = Objects.requireNonNull(schemaVisibilityConfig);
    +        mVisibleToConfigs = Objects.requireNonNull(visibleToConfigs);
    +    }
    +
    +    /**
    +     * Gets the schemaType for this VisibilityConfig.
    +     *
    +     * 

    This is being used as the document id when we convert a {@link InternalVisibilityConfig} + * to a {@link GenericDocument}. + */ + @NonNull + public String getSchemaType() { + return mSchemaType; + } + + /** Returns whether this schema is visible to the system. */ + public boolean isNotDisplayedBySystem() { + return mIsNotDisplayedBySystem; + } + + /** + * Returns the visibility settings stored in the public {@link SchemaVisibilityConfig} object. + */ + @NonNull + public SchemaVisibilityConfig getVisibilityConfig() { + return mVisibilityConfig; + } + + /** + * Returns required {@link SchemaVisibilityConfig} sets for a caller need to match to access the + * schema this {@link InternalVisibilityConfig} represents. + */ + @NonNull + public Set getVisibleToConfigs() { + return new ArraySet<>(mVisibleToConfigs); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + InternalVisibilityConfigCreator.writeToParcel(this, dest, flags); + } + + @Override + 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) + && Objects.equals(mVisibilityConfig, that.mVisibilityConfig) + && Objects.equals(mVisibleToConfigs, that.mVisibleToConfigs); + } + + @Override + public int hashCode() { + return Objects.hash(mIsNotDisplayedBySystem, mSchemaType, mVisibilityConfig, + mVisibleToConfigs); + } + + /** The builder class of {@link InternalVisibilityConfig}. */ + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + public static final class Builder { + private String mSchemaType; + private boolean mIsNotDisplayedBySystem; + private SchemaVisibilityConfig.Builder mVisibilityConfigBuilder; + private List mVisibleToConfigs = new ArrayList<>(); + private boolean mBuilt; + + /** + * Creates a {@link Builder} for a {@link InternalVisibilityConfig}. + * + * @param schemaType The SchemaType of the {@link AppSearchSchema} that this {@link + * InternalVisibilityConfig} represents. The package and database prefix + * will be added in server side. We are using prefixed schema type to be + * the final id of this {@link InternalVisibilityConfig}. This will be + * used as as an AppSearch id. + * @see GenericDocument#getId + */ + public Builder(@NonNull String schemaType) { + mSchemaType = Objects.requireNonNull(schemaType); + mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(); + } + + /** Creates a {@link Builder} from an existing {@link InternalVisibilityConfig} */ + public Builder(@NonNull InternalVisibilityConfig internalVisibilityConfig) { + Objects.requireNonNull(internalVisibilityConfig); + mSchemaType = internalVisibilityConfig.mSchemaType; + mIsNotDisplayedBySystem = internalVisibilityConfig.mIsNotDisplayedBySystem; + mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder( + internalVisibilityConfig.getVisibilityConfig()); + mVisibleToConfigs = internalVisibilityConfig.mVisibleToConfigs; + } + + /** Sets schemaType, which will be as the id when converting to {@link GenericDocument}. */ + @NonNull + @CanIgnoreReturnValue + public Builder setSchemaType(@NonNull String schemaType) { + resetIfBuilt(); + mSchemaType = Objects.requireNonNull(schemaType); + return this; + } + + /** + * Resets all values contained in the VisibilityConfig with the values from the given + * VisibiltiyConfig. + */ + @NonNull + @CanIgnoreReturnValue + public Builder setVisibilityConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) { + resetIfBuilt(); + mVisibilityConfigBuilder = new SchemaVisibilityConfig.Builder(schemaVisibilityConfig); + return this; + } + + /** + * Sets whether this schema has opted out of platform surfacing. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) { + resetIfBuilt(); + mIsNotDisplayedBySystem = notDisplayedBySystem; + return this; + } + + /** + * Add {@link PackageIdentifier} of packages which has access to this schema. + * + * @see SchemaVisibilityConfig.Builder#addAllowedPackage + */ + @CanIgnoreReturnValue + @NonNull + public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) { + resetIfBuilt(); + mVisibilityConfigBuilder.addAllowedPackage(packageIdentifier); + return this; + } + + /** + * Clears the list of packages which have access to this schema. + * + * @see SchemaVisibilityConfig.Builder#clearAllowedPackages + */ + @CanIgnoreReturnValue + @NonNull + public Builder clearVisibleToPackages() { + resetIfBuilt(); + mVisibilityConfigBuilder.clearAllowedPackages(); + return this; + } + + /** + * Adds a set of required Android {@link android.Manifest.permission} combination a package + * needs to hold to access the schema. + * + * @see SchemaVisibilityConfig.Builder#addRequiredPermissions + */ + @CanIgnoreReturnValue + @NonNull + public Builder addVisibleToPermissions(@NonNull Set visibleToPermissions) { + resetIfBuilt(); + mVisibilityConfigBuilder.addRequiredPermissions(visibleToPermissions); + return this; + } + + /** + * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}. + * + * @see SchemaVisibilityConfig.Builder#clearRequiredPermissions + */ + @CanIgnoreReturnValue + @NonNull + public Builder clearVisibleToPermissions() { + resetIfBuilt(); + mVisibilityConfigBuilder.clearRequiredPermissions(); + return this; + } + + /** + * Specify that this schema should be publicly available, to the same packages that have + * visibility to the package passed as a parameter. This visibility is determined by the + * result of {@link android.content.pm.PackageManager#canPackageQuery}. + * + * @see SchemaVisibilityConfig.Builder#setPubliclyVisibleTargetPackage + */ + @CanIgnoreReturnValue + @NonNull + public Builder setPubliclyVisibleTargetPackage( + @Nullable PackageIdentifier packageIdentifier) { + resetIfBuilt(); + mVisibilityConfigBuilder.setPubliclyVisibleTargetPackage(packageIdentifier); + return this; + } + + /** + * Add the {@link SchemaVisibilityConfig} for a caller need to match to access the schema + * this {@link InternalVisibilityConfig} represents. + * + *

    You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}, + * and the querier will have access if they match ANY of the + * {@link SchemaVisibilityConfig}. + * + * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} hold all requirements + * that a call must match to access the schema. + */ + @CanIgnoreReturnValue + @NonNull + public Builder addVisibleToConfig(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) { + Objects.requireNonNull(schemaVisibilityConfig); + resetIfBuilt(); + mVisibleToConfigs.add(schemaVisibilityConfig); + return this; + } + + /** Clears the set of {@link SchemaVisibilityConfig} which have access to this schema. */ + @CanIgnoreReturnValue + @NonNull + public Builder clearVisibleToConfig() { + resetIfBuilt(); + mVisibleToConfigs.clear(); + return this; + } + + private void resetIfBuilt() { + if (mBuilt) { + mVisibleToConfigs = new ArrayList<>(mVisibleToConfigs); + mBuilt = false; + } + } + + /** Build a {@link InternalVisibilityConfig} */ + @NonNull + public InternalVisibilityConfig build() { + mBuilt = true; + return new InternalVisibilityConfig( + mSchemaType, + mIsNotDisplayedBySystem, + mVisibilityConfigBuilder.build(), + mVisibleToConfigs); + } + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java
    new file mode 100644
    index 0000000..bd13255
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JetpackAppSearchEnvironment.java
    
    @@ -0,0 +1,105 @@
    +/*
    + * 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.
    + */
    +// @exportToFramework:skipFile()
    +package androidx.appsearch.app;
    +
    +import android.content.Context;
    +import android.os.UserHandle;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
    +
    +import java.io.File;
    +import java.util.concurrent.BlockingQueue;
    +import java.util.concurrent.ExecutorService;
    +import java.util.concurrent.Executors;
    +import java.util.concurrent.ThreadPoolExecutor;
    +import java.util.concurrent.TimeUnit;
    +
    +/**
    + * Contains utility methods for Framework implementation of AppSearch.
    + *
    + * @exportToFramework:hide
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +public class JetpackAppSearchEnvironment implements AppSearchEnvironment {
    +
    +    /**
    +     * Returns AppSearch directory in the credential encrypted system directory for the given user.
    +     *
    +     * 

    This folder should only be accessed after unlock. + */ + @Override + @NonNull + public File getAppSearchDir(@NonNull Context context, @Nullable UserHandle unused) { + return new File(context.getFilesDir(), "appsearch"); + } + + /** Creates context for the user based on the userHandle. */ + @Override + @NonNull + public Context createContextAsUser(@NonNull Context context, @NonNull UserHandle userHandle) { + return context; + } + + /** Creates and returns a ThreadPoolExecutor for given parameters. */ + @Override + @NonNull + public ExecutorService createExecutorService( + int corePoolSize, + int maxConcurrency, + long keepAliveTime, + @NonNull TimeUnit unit, + @NonNull BlockingQueue workQueue, + int priority) { + return new ThreadPoolExecutor( + corePoolSize, + maxConcurrency, + keepAliveTime, + unit, + workQueue); + } + + /** Creates and returns an ExecutorService with a single thread. */ + @Override + @NonNull + public ExecutorService createSingleThreadExecutor() { + return Executors.newSingleThreadExecutor(); + } + + /** Creates and returns an Executor with cached thread pools. */ + @Override + @NonNull + 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) { + return context.getCacheDir(); + } + + @Override + public boolean isInfoLoggingEnabled() { + // INFO logging is enabled by default in Jetpack AppSearch. + return true; + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
    index 3639a79..16aff14 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/JoinSpec.java
    
    @@ -16,16 +16,23 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.IntDef;
     import androidx.annotation.NonNull;
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.JoinSpecCreator;
     import androidx.core.util.Preconditions;
     
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
    +import java.util.Objects;
     
     /**
      * This class represents the specifications for the joining operation in search.
    @@ -117,12 +124,29 @@
      * return the signals calculated by scoring the joined documents using the scoring strategy in the
      * nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}.
      */
    -public final class JoinSpec {
    -    static final String NESTED_QUERY = "nestedQuery";
    -    static final String NESTED_SEARCH_SPEC = "nestedSearchSpec";
    -    static final String CHILD_PROPERTY_EXPRESSION = "childPropertyExpression";
    -    static final String MAX_JOINED_RESULT_COUNT = "maxJoinedResultCount";
    -    static final String AGGREGATION_SCORING_STRATEGY = "aggregationScoringStrategy";
    [email protected](creator = "JoinSpecCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class JoinSpec extends AbstractSafeParcelable {
    +    /** Creator class for {@link JoinSpec}. */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR = new JoinSpecCreator();
    +
    +    @Field(id = 1, getter = "getNestedQuery")
    +    private final String mNestedQuery;
    +
    +    @Field(id = 2, getter = "getNestedSearchSpec")
    +    private final SearchSpec mNestedSearchSpec;
    +
    +    @Field(id = 3, getter = "getChildPropertyExpression")
    +    private final String mChildPropertyExpression;
    +
    +    @Field(id = 4, getter = "getMaxJoinedResultCount")
    +    private final int mMaxJoinedResultCount;
    +
    +    @Field(id = 5, getter = "getAggregationScoringStrategy")
    +    private final int mAggregationScoringStrategy;
     
         private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10;
     
    @@ -158,8 +182,10 @@
         public @interface AggregationScoringStrategy {
         }
     
    -    /** Do not score the aggregation of joined documents. This is for the case where we want to
    -     * perform a join, but keep the parent ranking signal. */
    +    /**
    +     * Do not score the aggregation of joined documents. This is for the case where we want to
    +     * 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;
    @@ -172,33 +198,27 @@
         /** Score the aggregation of joined documents using the sum of ranking signal. */
         public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5;
     
    -    private final Bundle mBundle;
    -
    -    /** @exportToFramework:hide */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public JoinSpec(@NonNull Bundle bundle) {
    -        Preconditions.checkNotNull(bundle);
    -        mBundle = bundle;
    +    @Constructor
    +    JoinSpec(
    +            @Param(id = 1) @NonNull String nestedQuery,
    +            @Param(id = 2) @NonNull SearchSpec nestedSearchSpec,
    +            @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 = Objects.requireNonNull(childPropertyExpression);
    +        mMaxJoinedResultCount = maxJoinedResultCount;
    +        mAggregationScoringStrategy = aggregationScoringStrategy;
         }
     
    -    /**
    -     * Returns the {@link Bundle} populated by this builder.
    -     *
    -     * @exportToFramework:hide
    -     */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    -    }
     
         /**
          * Returns the query to run on the joined documents.
    -     *
          */
         @NonNull
         public String getNestedQuery() {
    -        return mBundle.getString(NESTED_QUERY);
    +        return mNestedQuery;
         }
     
         /**
    @@ -211,7 +231,7 @@
          */
         @NonNull
         public String getChildPropertyExpression() {
    -        return mBundle.getString(CHILD_PROPERTY_EXPRESSION);
    +        return mChildPropertyExpression;
         }
     
         /**
    @@ -219,7 +239,7 @@
          * with a default of 10 SearchResults.
          */
         public int getMaxJoinedResultCount() {
    -        return mBundle.getInt(MAX_JOINED_RESULT_COUNT);
    +        return mMaxJoinedResultCount;
         }
     
         /**
    @@ -231,7 +251,7 @@
          */
         @NonNull
         public SearchSpec getNestedSearchSpec() {
    -        return new SearchSpec(mBundle.getBundle(NESTED_SEARCH_SPEC));
    +        return mNestedSearchSpec;
         }
     
         /**
    @@ -244,7 +264,14 @@
          */
         @AggregationScoringStrategy
         public int getAggregationScoringStrategy() {
    -        return mBundle.getInt(AGGREGATION_SCORING_STRATEGY);
    +        return mAggregationScoringStrategy;
    +    }
    +
    +    @Override
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        JoinSpecCreator.writeToParcel(this, dest, flags);
         }
     
         /** Builder for {@link JoinSpec objects}. */
    @@ -257,7 +284,8 @@
             private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
             private final String mChildPropertyExpression;
             private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
    -        @AggregationScoringStrategy private int mAggregationScoringStrategy =
    +        @AggregationScoringStrategy
    +        private int mAggregationScoringStrategy =
                     AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL;
     
             /**
    @@ -289,6 +317,17 @@
                 mChildPropertyExpression = childPropertyExpression;
             }
     
    +        /** @exportToFramework:hide */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        public Builder(@NonNull JoinSpec joinSpec) {
    +            Preconditions.checkNotNull(joinSpec);
    +            mNestedQuery = joinSpec.getNestedQuery();
    +            mNestedSearchSpec = joinSpec.getNestedSearchSpec();
    +            mChildPropertyExpression = joinSpec.getChildPropertyExpression();
    +            mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount();
    +            mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy();
    +        }
    +
             /**
              * Sets the query and the SearchSpec for the documents being joined. This will score and
              * rank the joined documents as well as filter the joined documents.
    @@ -362,13 +401,13 @@
              */
             @NonNull
             public JoinSpec build() {
    -            Bundle bundle = new Bundle();
    -            bundle.putString(NESTED_QUERY, mNestedQuery);
    -            bundle.putBundle(NESTED_SEARCH_SPEC, mNestedSearchSpec.getBundle());
    -            bundle.putString(CHILD_PROPERTY_EXPRESSION, mChildPropertyExpression);
    -            bundle.putInt(MAX_JOINED_RESULT_COUNT, mMaxJoinedResultCount);
    -            bundle.putInt(AGGREGATION_SCORING_STRATEGY, mAggregationScoringStrategy);
    -            return new JoinSpec(bundle);
    +            return new JoinSpec(
    +                    mNestedQuery,
    +                    mNestedSearchSpec,
    +                    mChildPropertyExpression,
    +                    mMaxJoinedResultCount,
    +                    mAggregationScoringStrategy
    +            );
             }
         }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
    index 6c4bc59..c05d567 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
    
    @@ -16,20 +16,18 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    -
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
    -import androidx.appsearch.util.BundleUtil;
    +import androidx.appsearch.safeparcel.PackageIdentifierParcel;
     import androidx.core.util.Preconditions;
     
    -/** This class represents a uniquely identifiable package. */
    +/**
    + * This class represents a uniquely identifiable package.
    + */
     public class PackageIdentifier {
    -    private static final String PACKAGE_NAME_FIELD = "packageName";
    -    private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate";
    -
    -    private final Bundle mBundle;
    +    @NonNull
    +    private final PackageIdentifierParcel mPackageIdentifierParcel;
     
         /**
          * Creates a unique identifier for a package.
    @@ -43,36 +41,41 @@
          * new android.content.pm.Signature(outputDigest).toByteArray();
          * 
    * - * @param packageName Name of the package. + * @param packageName Name of the package. * @param sha256Certificate SHA-256 certificate digest of the package. */ public PackageIdentifier(@NonNull String packageName, @NonNull byte[] sha256Certificate) { - mBundle = new Bundle(); - mBundle.putString(PACKAGE_NAME_FIELD, packageName); - mBundle.putByteArray(SHA256_CERTIFICATE_FIELD, sha256Certificate); + Preconditions.checkNotNull(packageName); + Preconditions.checkNotNull(sha256Certificate); + mPackageIdentifierParcel = new PackageIdentifierParcel(packageName, sha256Certificate); } /** @exportToFramework:hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public PackageIdentifier(@NonNull Bundle bundle) { - mBundle = Preconditions.checkNotNull(bundle); + public PackageIdentifier(@NonNull PackageIdentifierParcel packageIdentifierParcel) { + mPackageIdentifierParcel = Preconditions.checkNotNull(packageIdentifierParcel); } - /** @exportToFramework:hide */ + /** + * Returns the {@link PackageIdentifierParcel} holding the values for this + * {@link PackageIdentifier}. + * + * @exportToFramework:hide + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull - public Bundle getBundle() { - return mBundle; + public PackageIdentifierParcel getPackageIdentifierParcel() { + return mPackageIdentifierParcel; } @NonNull public String getPackageName() { - return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD)); + return mPackageIdentifierParcel.getPackageName(); } @NonNull public byte[] getSha256Certificate() { - return Preconditions.checkNotNull(mBundle.getByteArray(SHA256_CERTIFICATE_FIELD)); + return mPackageIdentifierParcel.getSha256Certificate(); } @Override @@ -80,15 +83,15 @@ if (this == obj) { return true; } - if (obj == null || !(obj instanceof PackageIdentifier)) { + if (!(obj instanceof PackageIdentifier)) { return false; } final PackageIdentifier other = (PackageIdentifier) obj; - return BundleUtil.deepEquals(mBundle, other.mBundle); + return mPackageIdentifierParcel.equals(other.getPackageIdentifierParcel()); } @Override public int hashCode() { - return BundleUtil.deepHashCode(mBundle); + return mPackageIdentifierParcel.hashCode(); } }
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
    index e0557a6..0e69136 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PropertyPath.java
    
    @@ -20,6 +20,8 @@
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    +import androidx.appsearch.checker.initialization.qual.UnderInitialization;
    +import androidx.appsearch.checker.nullness.qual.RequiresNonNull;
     import androidx.core.util.ObjectsCompat;
     import androidx.core.util.Preconditions;
     
    @@ -73,7 +75,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) {
             Preconditions.checkNotNull(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 ObjectsCompat.equals(mPathList, that.mPathList);
         }
     
         @Override
         public int hashCode() {
    -        return ObjectsCompat.hash(mPathList);
    +        return ObjectsCompat.hashCode(mPathList);
         }
     
         /**
    @@ -292,9 +304,7 @@
                 mPropertyIndex = propertyIndex;
             }
     
    -        /**
    -         * @return the property name
    -         */
    +        /** Returns the name of the property. */
             @NonNull
             public String getPropertyName() {
                 return mPropertyName;
    @@ -319,10 +329,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/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
    index 6edf85e..e0332b4 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PutDocumentsRequest.java
    
    @@ -19,8 +19,13 @@
     import android.annotation.SuppressLint;
     
     import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
     import androidx.appsearch.exceptions.AppSearchException;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.usagereporting.TakenAction;
    +import androidx.collection.ArraySet;
     import androidx.core.util.Preconditions;
     
     import java.util.ArrayList;
    @@ -28,6 +33,7 @@
     import java.util.Collection;
     import java.util.Collections;
     import java.util.List;
    +import java.util.Set;
     
     /**
      * Encapsulates a request to index documents into an {@link AppSearchSession} database.
    @@ -43,8 +49,11 @@
     public final class PutDocumentsRequest {
         private final List mDocuments;
     
    -    PutDocumentsRequest(List documents) {
    +    private final List mTakenActions;
    +
    +    PutDocumentsRequest(List documents, List takenActions) {
             mDocuments = documents;
    +        mTakenActions = takenActions;
         }
     
         /** Returns a list of {@link GenericDocument} objects that are part of this request. */
    @@ -53,9 +62,27 @@
             return Collections.unmodifiableList(mDocuments);
         }
     
    +    /**
    +     * Returns a list of {@link GenericDocument} objects containing taken action metrics that are
    +     * part of this request.
    +     *
    +     * 
    +     * 

    See {@link Builder#addTakenActions(TakenAction...)}. + * + */ + @NonNull + @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS) + public List getTakenActionGenericDocuments() { + return Collections.unmodifiableList(mTakenActions); + } + + /** Builder for {@link PutDocumentsRequest} objects. */ public static final class Builder { private ArrayList mDocuments = new ArrayList<>(); + private ArrayList mTakenActions = new ArrayList<>(); private boolean mBuilt = false; /** Adds one or more {@link GenericDocument} objects to the request. */ @@ -121,18 +148,219 @@ } return addGenericDocuments(genericDocuments); } + + /** + * Adds one or more {@link TakenAction} objects to the request. + * + *

    Clients can construct {@link TakenAction} documents to report the user's actions on + * search results, and these actions can be used as signals to boost result ranking in + * future search requests. See {@link TakenAction} for more details. + * + *

    Clients should report search and click actions together sorted by + * {@link TakenAction#getActionTimestampMillis} in chronological order. + *

    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]. + * + *

    Certain anonymized information about actions reported using this API may be uploaded + * using statsd and may be used to improve the quality of the search algorithms. Most of + * the information in this class is already non-identifiable, such as durations and its + * position in the result set. Identifiable information which you choose to provide, such + * as the query string, will be anonymized using techniques like Federated Analytics to + * ensure only the most frequently searched terms across the whole user population are + * retained and available for study. + * + *

    You can alternatively use the {@link #addDocuments(Object...)} API with + * {@link TakenAction} document to retain the benefits of joining and using it on-device, + * without triggering any of the anonymized stats uploading described above. + * + * @param takenActions one or more {@link TakenAction} objects. + */ + // Merged list available from getTakenActionGenericDocuments() + @SuppressWarnings("MissingGetterMatchingBuilder") + @CanIgnoreReturnValue + @NonNull + public Builder addTakenActions( + @NonNull TakenAction... takenActions) throws AppSearchException { + Preconditions.checkNotNull(takenActions); + resetIfBuilt(); + return addTakenActions(Arrays.asList(takenActions)); + } + + /** + * Adds a collection of {@link TakenAction} objects to the request. + * + * @see #addTakenActions(TakenAction...) + * + * @param takenActions a collection of {@link TakenAction} objects. + */ + // Merged list available from getTakenActionGenericDocuments() + @SuppressWarnings("MissingGetterMatchingBuilder") + @CanIgnoreReturnValue + @NonNull + public Builder addTakenActions(@NonNull Collection takenActions) + throws AppSearchException { + Preconditions.checkNotNull(takenActions); + resetIfBuilt(); + List genericDocuments = new ArrayList<>(takenActions.size()); + for (Object takenAction : takenActions) { + GenericDocument genericDocument = GenericDocument.fromDocumentClass(takenAction); + genericDocuments.add(genericDocument); + } + mTakenActions.addAll(genericDocuments); + return this; + } // @exportToFramework:endStrip() - /** Creates a new {@link PutDocumentsRequest} object. */ + /** + * Adds one or more {@link GenericDocument} objects containing taken action metrics to the + * request. + * + *

    It is recommended to use taken action document classes in Jetpack library to construct + * taken action documents. + * + *

    The document creation timestamp of the {@link GenericDocument} should be set to the + * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}. + * + *

    Clients should report search and click actions together sorted by + * {@link GenericDocument#getCreationTimestampMillis} in chronological order. + *

    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]. + * + *

    Different types of taken actions and metrics to be collected by AppSearch: + *

      + *
    • + * Search action + *
        + *
      • actionType: LONG, the enum value of the action type. + *

        Requires to be {@code 1} for search actions. + * + *

      • query: STRING, the user-entered search input (without any operators or + * rewriting). + * + *
      • fetchedResultCount: LONG, the number of {@link SearchResult} documents + * fetched from AppSearch in this search action. + *
      + *
    • + * + *
    • + * Click action + *
        + *
      • actionType: LONG, the enum value of the action type. + *

        Requires to be {@code 2} for click actions. + * + *

      • query: STRING, the user-entered search input (without any operators or + * rewriting) that yielded the {@link SearchResult} on which the user took action. + * + *
      • referencedQualifiedId: STRING, the qualified id of the {@link SearchResult} + * document that the user takes action on. + *

        A qualified id is a string generated by package, database, namespace, and + * document id. See + * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId} for more + * details. + * + *

      • resultRankInBlock: LONG, the rank of the {@link SearchResult} document among + * the user-defined block. + *

        The client can define its own custom definition for block, for example, + * corpus name, group, etc. + *

        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]. + *

        If the client is not presenting the results in multiple blocks, they should + * set this value to match resultRankGlobal. + * + *

      • resultRankGlobal: LONG, the global rank of the {@link SearchResult} + * document. + *

        Global rank reflects the order of {@link SearchResult} documents returned by + * AppSearch. + *

        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. + * + *

      • timeStayOnResultMillis: LONG, the time in milliseconds that user stays on + * the {@link SearchResult} document after clicking it. + *
      + *
    • + *
    + * + *

    Certain anonymized information about actions reported using this API may be uploaded + * using statsd and may be used to improve the quality of the search algorithms. Most of + * the information in this class is already non-identifiable, such as durations and its + * position in the result set. Identifiable information which you choose to provide, such + * as the query string, will be anonymized using techniques like Federated Analytics to + * ensure only the most frequently searched terms across the whole user population are + * retained and available for study. + * + *

    You can alternatively use the {@link #addGenericDocuments(GenericDocument...)} API to + * retain the benefits of joining and using it on-device, without triggering any of the + * anonymized stats uploading described above. + * + * @param takenActionGenericDocuments one or more {@link GenericDocument} objects containing + * taken action metric fields. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @CanIgnoreReturnValue + @NonNull + @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS) + public Builder addTakenActionGenericDocuments( + @NonNull GenericDocument... takenActionGenericDocuments) throws AppSearchException { + Preconditions.checkNotNull(takenActionGenericDocuments); + resetIfBuilt(); + return addTakenActionGenericDocuments(Arrays.asList(takenActionGenericDocuments)); + } + + /** + * Adds a collection of {@link GenericDocument} objects containing taken action metrics to + * the request. + * + * @see #addTakenActionGenericDocuments(GenericDocument...) + * + * @param takenActionGenericDocuments a collection of {@link GenericDocument} objects + * containing taken action metric fields. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @CanIgnoreReturnValue + @NonNull + @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS) + public Builder addTakenActionGenericDocuments(@NonNull Collection + extends GenericDocument> takenActionGenericDocuments) throws AppSearchException { + Preconditions.checkNotNull(takenActionGenericDocuments); + resetIfBuilt(); + mTakenActions.addAll(takenActionGenericDocuments); + return this; + } + + /** + * Creates a new {@link PutDocumentsRequest} object. + * + * @throws IllegalArgumentException if there is any id collision between normal and action + * documents. + */ @NonNull public PutDocumentsRequest build() { mBuilt = true; - return new PutDocumentsRequest(mDocuments); + + // Verify there is no id collision between normal documents and action documents. + Set idSet = new ArraySet<>(); + for (int i = 0; i < mDocuments.size(); i++) { + idSet.add(mDocuments.get(i).getId()); + } + for (int i = 0; i < mTakenActions.size(); i++) { + GenericDocument takenAction = mTakenActions.get(i); + if (idSet.contains(takenAction.getId())) { + throw new IllegalArgumentException("Document id " + takenAction.getId() + + " cannot exist in both taken action and normal document"); + } + } + + return new PutDocumentsRequest(mDocuments, mTakenActions); } private void resetIfBuilt() { if (mBuilt) { mDocuments = new ArrayList<>(mDocuments); + mTakenActions = new ArrayList<>(mTakenActions); mBuilt = false; } }

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
    index 40cd591..ce76455 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByDocumentIdRequest.java
    
    @@ -16,14 +16,27 @@
     
     package androidx.appsearch.app;
     
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
     import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.RemoveByDocumentIdRequestCreator;
     import androidx.collection.ArraySet;
     import androidx.core.util.Preconditions;
     
    +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 +45,37 @@
      *
      * @see AppSearchSession#removeAsync
      */
    -public final class RemoveByDocumentIdRequest {
    -    private final String mNamespace;
    -    private final Set mIds;
    [email protected](creator = "RemoveByDocumentIdRequestCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class RemoveByDocumentIdRequest extends AbstractSafeParcelable {
    +    /** Creator class for {@link android.app.appsearch.RemoveByDocumentIdRequest}. */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR =
    +            new RemoveByDocumentIdRequestCreator();
     
    -    RemoveByDocumentIdRequest(String namespace, Set ids) {
    -        mNamespace = namespace;
    -        mIds = ids;
    +    @NonNull
    +    @Field(id = 1, getter = "getNamespace")
    +    private final String mNamespace;
    +    @NonNull
    +    @Field(id = 2)
    +    final List mIds;
    +    @Nullable
    +    private Set 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 ids) {
    +        mNamespace = Objects.requireNonNull(namespace);
    +        mIds = Objects.requireNonNull(ids);
         }
     
         /** Returns the namespace to remove documents from. */
    @@ -50,7 +87,17 @@
         /** Returns the set of document IDs attached to the request. */
         @NonNull
         public Set getIds() {
    -        return Collections.unmodifiableSet(mIds);
    +        if (mIdsCached == null) {
    +            mIdsCached = Collections.unmodifiableSet(new ArraySet<>(mIds));
    +        }
    +        return mIdsCached;
    +    }
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @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 +134,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/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
    index 58e7d9b..aafcc61 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/ReportUsageRequest.java
    
    @@ -16,10 +16,21 @@
     
     package androidx.appsearch.app;
     
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
     import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.ReportUsageRequestCreator;
     import androidx.core.util.Preconditions;
     
    +import java.util.Objects;
    +
     /**
      * A request to report usage of a document.
      *
    @@ -27,18 +38,34 @@
      *
      * @see AppSearchSession#reportUsageAsync
      */
    -public final class ReportUsageRequest {
    -    private final String mNamespace;
    -    private final String mDocumentId;
    -    private final long mUsageTimestampMillis;
    +@SuppressWarnings("HiddenSuperclass")
    [email protected](creator = "ReportUsageRequestCreator")
    +public final class ReportUsageRequest extends AbstractSafeParcelable {
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull public static final Parcelable.Creator 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) {
    -        mNamespace = Preconditions.checkNotNull(namespace);
    -        mDocumentId = Preconditions.checkNotNull(documentId);
    +            @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;
         }
     
    +
         /** Returns the namespace of the document that was used. */
         @NonNull
         public String getNamespace() {
    @@ -62,6 +89,13 @@
             return mUsageTimestampMillis;
         }
     
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @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/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java
    new file mode 100644
    index 0000000..32b69b3
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SchemaVisibilityConfig.java
    
    @@ -0,0 +1,294 @@
    +/*
    + * 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 androidx.appsearch.app;
    +
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.PackageIdentifierParcel;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityConfigCreator;
    +import androidx.collection.ArraySet;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Objects;
    +import java.util.Set;
    +
    +/**
    + * A class to hold a all necessary Visibility information corresponding to the same schema. This
    + * pattern allows for easier association of these documents.
    + *
    + * 

    This does not correspond to any schema, the properties held in this class are kept in two + * separate schemas, VisibilityConfig and PublicAclOverlay. + */ +@FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) [email protected](creator = "VisibilityConfigCreator") +@SuppressWarnings("HiddenSuperclass") +public final class SchemaVisibilityConfig extends AbstractSafeParcelable { + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static final Parcelable.Creator CREATOR = + new VisibilityConfigCreator(); + + @NonNull + @Field(id = 1) + final List mAllowedPackages; + + @NonNull + @Field(id = 2) + final List mRequiredPermissions; + + @Nullable + @Field(id = 3) + final PackageIdentifierParcel mPubliclyVisibleTargetPackage; + + @Nullable private Integer mHashCode; + @Nullable private List mAllowedPackagesCached; + @Nullable private Set> mRequiredPermissionsCached; + + @Constructor + SchemaVisibilityConfig( + @Param(id = 1) @NonNull List allowedPackages, + @Param(id = 2) @NonNull List requiredPermissions, + @Param(id = 3) @Nullable PackageIdentifierParcel publiclyVisibleTargetPackage) { + mAllowedPackages = Objects.requireNonNull(allowedPackages); + mRequiredPermissions = Objects.requireNonNull(requiredPermissions); + mPubliclyVisibleTargetPackage = publiclyVisibleTargetPackage; + } + + /** Returns a list of {@link PackageIdentifier}s of packages that can access this schema. */ + @NonNull + public List getAllowedPackages() { + if (mAllowedPackagesCached == null) { + mAllowedPackagesCached = new ArrayList<>(mAllowedPackages.size()); + for (int i = 0; i < mAllowedPackages.size(); i++) { + mAllowedPackagesCached.add(new PackageIdentifier(mAllowedPackages.get(i))); + } + } + return mAllowedPackagesCached; + } + + /** + * 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> getRequiredPermissions() { + if (mRequiredPermissionsCached == null) { + mRequiredPermissionsCached = new ArraySet<>(mRequiredPermissions.size()); + for (int i = 0; i < mRequiredPermissions.size(); i++) { + VisibilityPermissionConfig permissionConfig = mRequiredPermissions.get(i); + Set requiredPermissions = permissionConfig.getAllRequiredPermissions(); + if (mRequiredPermissionsCached != null && requiredPermissions != null) { + mRequiredPermissionsCached.add(requiredPermissions); + } + } + } + // Added for nullness checker as it is @Nullable, we initialize it above if it is null. + return Objects.requireNonNull(mRequiredPermissionsCached); + } + + /** + * Returns the {@link PackageIdentifier} of the package that will be used as the target package + * in a call to {@link android.content.pm.PackageManager#canPackageQuery} to determine which + * packages can access this publicly visible schema. Returns null if the schema is not publicly + * visible. + */ + @Nullable + public PackageIdentifier getPubliclyVisibleTargetPackage() { + if (mPubliclyVisibleTargetPackage == null) { + return null; + } + return new PackageIdentifier(mPubliclyVisibleTargetPackage); + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + VisibilityConfigCreator.writeToParcel(this, dest, flags); + } + + @Override + 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) + && Objects.equals( + mPubliclyVisibleTargetPackage, that.mPubliclyVisibleTargetPackage); + } + + @Override + public int hashCode() { + if (mHashCode == null) { + mHashCode = Objects.hash( + mAllowedPackages, + mRequiredPermissions, + mPubliclyVisibleTargetPackage); + } + return mHashCode; + } + + /** The builder class of {@link SchemaVisibilityConfig}. */ + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + public static final class Builder { + private List mAllowedPackages = new ArrayList<>(); + private List mRequiredPermissions = new ArrayList<>(); + @Nullable private PackageIdentifierParcel mPubliclyVisibleTargetPackage; + private boolean mBuilt; + + /** Creates a {@link Builder} for a {@link SchemaVisibilityConfig}. */ + public Builder() {} + + /** + * Creates a {@link Builder} copying the values from an existing + * {@link SchemaVisibilityConfig}. + * + * @exportToFramework:hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public Builder(@NonNull SchemaVisibilityConfig schemaVisibilityConfig) { + Objects.requireNonNull(schemaVisibilityConfig); + mAllowedPackages = new ArrayList<>(schemaVisibilityConfig.mAllowedPackages); + mRequiredPermissions = new ArrayList<>(schemaVisibilityConfig.mRequiredPermissions); + mPubliclyVisibleTargetPackage = schemaVisibilityConfig.mPubliclyVisibleTargetPackage; + } + + /** Add {@link PackageIdentifier} of packages which has access to this schema. */ + @CanIgnoreReturnValue + @NonNull + public Builder addAllowedPackage(@NonNull PackageIdentifier packageIdentifier) { + Objects.requireNonNull(packageIdentifier); + resetIfBuilt(); + mAllowedPackages.add(packageIdentifier.getPackageIdentifierParcel()); + return this; + } + + /** Clears the list of packages which have access to this schema. */ + @CanIgnoreReturnValue + @NonNull + public Builder clearAllowedPackages() { + resetIfBuilt(); + mAllowedPackages.clear(); + return this; + } + + /** + * Adds a set of required Android {@link android.Manifest.permission} combination a + * package needs to hold to access the schema this {@link SchemaVisibilityConfig} + * represents. + * + *

    If the querier holds ALL of the required permissions in this combination, they will + * have access to read {@link GenericDocument} objects of the given schema type. + * + *

    You can call this method repeatedly to add multiple permission combinations, and the + * querier will have access if they holds ANY of the combinations. + * + *

    Merged Set available from {@link #getRequiredPermissions()}. + * + * @see SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility for + * supported Permissions. + */ + @SuppressWarnings("RequiresPermission") // No permission required to call this method + @CanIgnoreReturnValue + @NonNull + public Builder addRequiredPermissions(@NonNull Set visibleToPermissions) { + Objects.requireNonNull(visibleToPermissions); + resetIfBuilt(); + mRequiredPermissions.add(new VisibilityPermissionConfig(visibleToPermissions)); + return this; + } + + /** + * Clears all required permissions combinations set to this {@link SchemaVisibilityConfig}. + */ + @CanIgnoreReturnValue + @NonNull + public Builder clearRequiredPermissions() { + resetIfBuilt(); + mRequiredPermissions.clear(); + return this; + } + + /** + * Specify that this schema should be publicly available, to the same packages that have + * visibility to the package passed as a parameter. This visibility is determined by the + * result of {@link android.content.pm.PackageManager#canPackageQuery}. + * + *

    It is possible for the packageIdentifier parameter to be different from the + * package performing the indexing. This might happen in the case of an on-device indexer + * processing information about various packages. The visibility will be the same + * regardless of which package indexes the document, as the visibility is based on the + * packageIdentifier parameter. + * + *

    Calling this with packageIdentifier set to null is valid, and will remove public + * visibility for the schema. + * + * @param packageIdentifier the {@link PackageIdentifier} of the package that will be used + * as the target package in a call to {@link + * android.content.pm.PackageManager#canPackageQuery} to determine + * which packages can access this publicly visible schema. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setPubliclyVisibleTargetPackage( + @Nullable PackageIdentifier packageIdentifier) { + resetIfBuilt(); + if (packageIdentifier == null) { + mPubliclyVisibleTargetPackage = null; + } else { + mPubliclyVisibleTargetPackage = packageIdentifier.getPackageIdentifierParcel(); + } + return this; + } + + private void resetIfBuilt() { + if (mBuilt) { + mAllowedPackages = new ArrayList<>(mAllowedPackages); + mRequiredPermissions = new ArrayList<>(mRequiredPermissions); + mBuilt = false; + } + } + + /** Build a {@link SchemaVisibilityConfig} */ + @NonNull + public SchemaVisibilityConfig build() { + mBuilt = true; + return new SchemaVisibilityConfig( + mAllowedPackages, + mRequiredPermissions, + mPubliclyVisibleTargetPackage); + } + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
    index 187304f..0fce482 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
    
    @@ -16,7 +16,8 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    @@ -24,10 +25,18 @@
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
     import androidx.appsearch.exceptions.AppSearchException;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.GenericDocumentParcel;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.MatchInfoCreator;
    +import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultCreator;
     import androidx.core.util.ObjectsCompat;
     import androidx.core.util.Preconditions;
     
     import java.util.ArrayList;
    +import java.util.Collections;
     import java.util.List;
     import java.util.Map;
     
    @@ -46,36 +55,61 @@
      *
      * @see SearchResults
      */
    -public final class SearchResult {
    -    static final String DOCUMENT_FIELD = "document";
    -    static final String MATCH_INFOS_FIELD = "matchInfos";
    -    static final String PACKAGE_NAME_FIELD = "packageName";
    -    static final String DATABASE_NAME_FIELD = "databaseName";
    -    static final String RANKING_SIGNAL_FIELD = "rankingSignal";
    -    static final String JOINED_RESULTS = "joinedResults";
    [email protected](creator = "SearchResultCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class SearchResult extends AbstractSafeParcelable {
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull public static final Parcelable.Creator CREATOR =
    +            new SearchResultCreator();
     
    +    @Field(id = 1)
    +    final GenericDocumentParcel mDocument;
    +    @Field(id = 2)
    +    final List mMatchInfos;
    +    @Field(id = 3, getter = "getPackageName")
    +    private final String mPackageName;
    +    @Field(id = 4, getter = "getDatabaseName")
    +    private final String mDatabaseName;
    +    @Field(id = 5, getter = "getRankingSignal")
    +    private final double mRankingSignal;
    +    @Field(id = 6, getter = "getJoinedResults")
    +    private final List mJoinedResults;
         @NonNull
    -    private final Bundle mBundle;
    +    @Field(id = 7, getter = "getInformationalRankingSignals")
    +    private final List mInformationalRankingSignals;
     
    -    /** Cache of the inflated document. Comes from inflating mDocumentBundle at first use. */
    -    @Nullable
    -    private GenericDocument mDocument;
     
    -    /** Cache of the inflated matches. Comes from inflating mMatchBundles at first use. */
    +    /** Cache of the {@link GenericDocument}. Comes from mDocument at first use. */
         @Nullable
    -    private List mMatchInfos;
    +    private GenericDocument mDocumentCached;
    +
    +    /** Cache of the inflated {@link MatchInfo}. Comes from inflating mMatchInfos at first use. */
    +    @Nullable
    +    private List mMatchInfosCached;
     
         /** @exportToFramework:hide */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public SearchResult(@NonNull Bundle bundle) {
    -        mBundle = Preconditions.checkNotNull(bundle);
    -    }
    -
    -    /** @exportToFramework:hide */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    +    @Constructor
    +    SearchResult(
    +            @Param(id = 1) @NonNull GenericDocumentParcel document,
    +            @Param(id = 2) @NonNull List matchInfos,
    +            @Param(id = 3) @NonNull String packageName,
    +            @Param(id = 4) @NonNull String databaseName,
    +            @Param(id = 5) double rankingSignal,
    +            @Param(id = 6) @NonNull List joinedResults,
    +            @Param(id = 7) @Nullable List informationalRankingSignals) {
    +        mDocument = Preconditions.checkNotNull(document);
    +        mMatchInfos = Preconditions.checkNotNull(matchInfos);
    +        mPackageName = Preconditions.checkNotNull(packageName);
    +        mDatabaseName = Preconditions.checkNotNull(databaseName);
    +        mRankingSignal = rankingSignal;
    +        mJoinedResults = Collections.unmodifiableList(Preconditions.checkNotNull(joinedResults));
    +        if (informationalRankingSignals != null) {
    +            mInformationalRankingSignals = Collections.unmodifiableList(
    +                    informationalRankingSignals);
    +        } else {
    +            mInformationalRankingSignals = Collections.emptyList();
    +        }
         }
     
     // @exportToFramework:startStrip()
    @@ -92,7 +126,7 @@
          * @see GenericDocument#toDocumentClass(Class)
          */
         @NonNull
    -    public  T getDocument(@NonNull Class documentClass) throws AppSearchException {
    +    public  T getDocument(@NonNull java.lang.Class documentClass) throws AppSearchException {
             return getDocument(documentClass, /* documentClassMap= */null);
         }
     
    @@ -112,7 +146,7 @@
          * @see GenericDocument#toDocumentClass(Class, Map)
          */
         @NonNull
    -    public  T getDocument(@NonNull Class documentClass,
    +    public  T getDocument(@NonNull java.lang.Class documentClass,
                 @Nullable Map> documentClassMap) throws AppSearchException {
             Preconditions.checkNotNull(documentClass);
             return getGenericDocument().toDocumentClass(documentClass, documentClassMap);
    @@ -126,11 +160,10 @@
          */
         @NonNull
         public GenericDocument getGenericDocument() {
    -        if (mDocument == null) {
    -            mDocument = new GenericDocument(
    -                    Preconditions.checkNotNull(mBundle.getBundle(DOCUMENT_FIELD)));
    +        if (mDocumentCached == null) {
    +            mDocumentCached = new GenericDocument(mDocument);
             }
    -        return mDocument;
    +        return mDocumentCached;
         }
     
         /**
    @@ -143,22 +176,21 @@
          * value, this method returns an empty list.
          */
         @NonNull
    -    @SuppressWarnings("deprecation")
         public List getMatchInfos() {
    -        if (mMatchInfos == null) {
    -            List matchBundles =
    -                    Preconditions.checkNotNull(mBundle.getParcelableArrayList(MATCH_INFOS_FIELD));
    -            mMatchInfos = new ArrayList<>(matchBundles.size());
    -            for (int i = 0; i < matchBundles.size(); i++) {
    -                MatchInfo matchInfo = new MatchInfo(matchBundles.get(i), getGenericDocument());
    -                if (mMatchInfos != null) {
    +        if (mMatchInfosCached == null) {
    +            mMatchInfosCached = new ArrayList<>(mMatchInfos.size());
    +            for (int i = 0; i < mMatchInfos.size(); i++) {
    +                MatchInfo matchInfo = mMatchInfos.get(i);
    +                matchInfo.setDocument(getGenericDocument());
    +                if (mMatchInfosCached != null) {
                         // This additional check is added for NullnessChecker.
    -                    mMatchInfos.add(matchInfo);
    +                    mMatchInfosCached.add(matchInfo);
                     }
                 }
    +            mMatchInfosCached = Collections.unmodifiableList(mMatchInfosCached);
             }
             // This check is added for NullnessChecker, mMatchInfos will always be NonNull.
    -        return Preconditions.checkNotNull(mMatchInfos);
    +        return Preconditions.checkNotNull(mMatchInfosCached);
         }
     
         /**
    @@ -168,7 +200,7 @@
          */
         @NonNull
         public String getPackageName() {
    -        return Preconditions.checkNotNull(mBundle.getString(PACKAGE_NAME_FIELD));
    +        return mPackageName;
         }
     
         /**
    @@ -178,7 +210,7 @@
          */
         @NonNull
         public String getDatabaseName() {
    -        return Preconditions.checkNotNull(mBundle.getString(DATABASE_NAME_FIELD));
    +        return mDatabaseName;
         }
     
         /**
    @@ -207,7 +239,17 @@
          * @return Ranking signal of the document
          */
         public double getRankingSignal() {
    -        return mBundle.getDouble(RANKING_SIGNAL_FIELD);
    +        return mRankingSignal;
    +    }
    +
    +    /**
    +     * 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 getInformationalRankingSignals() {
    +        return mInformationalRankingSignals;
         }
     
         /**
    @@ -225,28 +267,26 @@
          * @return a List of SearchResults containing joined documents.
          */
         @NonNull
    -    @SuppressWarnings("deprecation") // Bundle#getParcelableArrayList(String) is deprecated.
         public List getJoinedResults() {
    -        ArrayList bundles = mBundle.getParcelableArrayList(JOINED_RESULTS);
    -        if (bundles == null) {
    -            return new ArrayList<>();
    -        }
    -        List res = new ArrayList<>(bundles.size());
    -        for (int i = 0; i < bundles.size(); i++) {
    -            res.add(new SearchResult(bundles.get(i)));
    -        }
    +        return mJoinedResults;
    +    }
     
    -        return res;
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        SearchResultCreator.writeToParcel(this, dest, flags);
         }
     
         /** Builder for {@link SearchResult} objects. */
         public static final class Builder {
             private final String mPackageName;
             private final String mDatabaseName;
    -        private ArrayList mMatchInfoBundles = new ArrayList<>();
    +        private List mMatchInfos = new ArrayList<>();
             private GenericDocument mGenericDocument;
             private double mRankingSignal;
    -        private ArrayList mJoinedResults = new ArrayList<>();
    +        private List mInformationalRankingSignals = new ArrayList<>();
    +        private List mJoinedResults = new ArrayList<>();
             private boolean mBuilt = false;
     
             /**
    @@ -260,6 +300,26 @@
                 mDatabaseName = Preconditions.checkNotNull(databaseName);
             }
     
    +        /** @exportToFramework:hide */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        public Builder(@NonNull SearchResult searchResult) {
    +            Preconditions.checkNotNull(searchResult);
    +            mPackageName = searchResult.getPackageName();
    +            mDatabaseName = searchResult.getDatabaseName();
    +            mGenericDocument = searchResult.getGenericDocument();
    +            mRankingSignal = searchResult.getRankingSignal();
    +            mInformationalRankingSignals = new ArrayList<>(
    +                    searchResult.getInformationalRankingSignals());
    +            List matchInfos = searchResult.getMatchInfos();
    +            for (int i = 0; i < matchInfos.size(); i++) {
    +                addMatchInfo(new MatchInfo.Builder(matchInfos.get(i)).build());
    +            }
    +            List joinedResults = searchResult.getJoinedResults();
    +            for (int i = 0; i < joinedResults.size(); i++) {
    +                addJoinedResult(joinedResults.get(i));
    +            }
    +        }
    +
     // @exportToFramework:startStrip()
             /**
              * Sets the document which matched.
    @@ -298,7 +358,7 @@
                         "This MatchInfo is already associated with a SearchResult and can't be "
                                 + "reassigned");
                 resetIfBuilt();
    -            mMatchInfoBundles.add(matchInfo.mBundle);
    +            mMatchInfos.add(matchInfo);
                 return this;
             }
     
    @@ -311,6 +371,17 @@
                 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}.
              * @param joinedResult The joined SearchResult to add.
    @@ -319,28 +390,43 @@
             @NonNull
             public Builder addJoinedResult(@NonNull SearchResult joinedResult) {
                 resetIfBuilt();
    -            mJoinedResults.add(joinedResult.getBundle());
    +            mJoinedResults.add(joinedResult);
    +            return this;
    +        }
    +
    +        /**
    +         * Clears the {@link SearchResult}s that were joined.
    +         *
    +         * @exportToFramework:hide
    +         */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        @CanIgnoreReturnValue
    +        @NonNull
    +        public Builder clearJoinedResults() {
    +            resetIfBuilt();
    +            mJoinedResults.clear();
                 return this;
             }
     
             /** Constructs a new {@link SearchResult}. */
             @NonNull
             public SearchResult build() {
    -            Bundle bundle = new Bundle();
    -            bundle.putString(PACKAGE_NAME_FIELD, mPackageName);
    -            bundle.putString(DATABASE_NAME_FIELD, mDatabaseName);
    -            bundle.putBundle(DOCUMENT_FIELD, mGenericDocument.getBundle());
    -            bundle.putDouble(RANKING_SIGNAL_FIELD, mRankingSignal);
    -            bundle.putParcelableArrayList(MATCH_INFOS_FIELD, mMatchInfoBundles);
    -            bundle.putParcelableArrayList(JOINED_RESULTS, mJoinedResults);
                 mBuilt = true;
    -            return new SearchResult(bundle);
    +            return new SearchResult(
    +                    mGenericDocument.getDocumentParcel(),
    +                    mMatchInfos,
    +                    mPackageName,
    +                    mDatabaseName,
    +                    mRankingSignal,
    +                    mJoinedResults,
    +                    mInformationalRankingSignals);
             }
     
             private void resetIfBuilt() {
                 if (mBuilt) {
    -                mMatchInfoBundles = new ArrayList<>(mMatchInfoBundles);
    +                mMatchInfos = new ArrayList<>(mMatchInfos);
                     mJoinedResults = new ArrayList<>(mJoinedResults);
    +                mInformationalRankingSignals = new ArrayList<>(mInformationalRankingSignals);
                     mBuilt = false;
                 }
             }
    @@ -410,20 +496,32 @@
          *      
  • {@link MatchInfo#getSnippet()} returns "Testing 1"
  • * */ - public static final class MatchInfo { - /** The path of the matching snippet property. */ - private static final String PROPERTY_PATH_FIELD = "propertyPath"; - private static final String EXACT_MATCH_RANGE_LOWER_FIELD = "exactMatchRangeLower"; - private static final String EXACT_MATCH_RANGE_UPPER_FIELD = "exactMatchRangeUpper"; - private static final String SUBMATCH_RANGE_LOWER_FIELD = "submatchRangeLower"; - private static final String SUBMATCH_RANGE_UPPER_FIELD = "submatchRangeUpper"; - private static final String SNIPPET_RANGE_LOWER_FIELD = "snippetRangeLower"; - private static final String SNIPPET_RANGE_UPPER_FIELD = "snippetRangeUpper"; + @SafeParcelable.Class(creator = "MatchInfoCreator") + @SuppressWarnings("HiddenSuperclass") + public static final class MatchInfo extends AbstractSafeParcelable { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @NonNull public static final Parcelable.Creator CREATOR = + new MatchInfoCreator(); + /** The path of the matching snippet property. */ + @Field(id = 1, getter = "getPropertyPath") private final String mPropertyPath; + @Field(id = 2) + final int mExactMatchRangeStart; + @Field(id = 3) + final int mExactMatchRangeEnd; + @Field(id = 4) + final int mSubmatchRangeStart; + @Field(id = 5) + final int mSubmatchRangeEnd; + @Field(id = 6) + final int mSnippetRangeStart; + @Field(id = 7) + final int mSnippetRangeEnd; + @Nullable private PropertyPath mPropertyPathObject = null; - final Bundle mBundle; /** * Document which the match comes from. @@ -432,7 +530,7 @@ * {@link #getExactMatch}, will throw {@link NullPointerException}. */ @Nullable - final GenericDocument mDocument; + private GenericDocument mDocument = null; /** Full text of the matched property. Populated on first use. */ @Nullable @@ -440,23 +538,35 @@ /** Range of property that exactly matched the query. Populated on first use. */ @Nullable - private MatchRange mExactMatchRange; + private MatchRange mExactMatchRangeCached; /** * Range of property that corresponds to the subsequence of the exact match that directly * matches a query term. Populated on first use. */ @Nullable - private MatchRange mSubmatchRange; + private MatchRange mSubmatchRangeCached; /** Range of some reasonable amount of context around the query. Populated on first use. */ @Nullable - private MatchRange mWindowRange; + private MatchRange mWindowRangeCached; - MatchInfo(@NonNull Bundle bundle, @Nullable GenericDocument document) { - mBundle = Preconditions.checkNotNull(bundle); - mDocument = document; - mPropertyPath = Preconditions.checkNotNull(bundle.getString(PROPERTY_PATH_FIELD)); + @Constructor + MatchInfo( + @Param(id = 1) @NonNull String propertyPath, + @Param(id = 2) int exactMatchRangeStart, + @Param(id = 3) int exactMatchRangeEnd, + @Param(id = 4) int submatchRangeStart, + @Param(id = 5) int submatchRangeEnd, + @Param(id = 6) int snippetRangeStart, + @Param(id = 7) int snippetRangeEnd) { + mPropertyPath = Preconditions.checkNotNull(propertyPath); + mExactMatchRangeStart = exactMatchRangeStart; + mExactMatchRangeEnd = exactMatchRangeEnd; + mSubmatchRangeStart = submatchRangeStart; + mSubmatchRangeEnd = submatchRangeEnd; + mSnippetRangeStart = snippetRangeStart; + mSnippetRangeEnd = snippetRangeEnd; } /** @@ -521,12 +631,12 @@ */ @NonNull public MatchRange getExactMatchRange() { - if (mExactMatchRange == null) { - mExactMatchRange = new MatchRange( - mBundle.getInt(EXACT_MATCH_RANGE_LOWER_FIELD), - mBundle.getInt(EXACT_MATCH_RANGE_UPPER_FIELD)); + if (mExactMatchRangeCached == null) { + mExactMatchRangeCached = new MatchRange( + mExactMatchRangeStart, + mExactMatchRangeEnd); } - return mExactMatchRange; + return mExactMatchRangeCached; } /** @@ -555,20 +665,18 @@ * false. * */ - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) - // @exportToFramework:endStrip() @NonNull public MatchRange getSubmatchRange() { checkSubmatchSupported(); - if (mSubmatchRange == null) { - mSubmatchRange = new MatchRange( - mBundle.getInt(SUBMATCH_RANGE_LOWER_FIELD), - mBundle.getInt(SUBMATCH_RANGE_UPPER_FIELD)); + if (mSubmatchRangeCached == null) { + mSubmatchRangeCached = new MatchRange( + mSubmatchRangeStart, + mSubmatchRangeEnd); } - return mSubmatchRange; + return mSubmatchRangeCached; } /** @@ -585,11 +693,9 @@ * false. * */ - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) - // @exportToFramework:endStrip() @NonNull public CharSequence getSubmatch() { checkSubmatchSupported(); @@ -606,12 +712,12 @@ */ @NonNull public MatchRange getSnippetRange() { - if (mWindowRange == null) { - mWindowRange = new MatchRange( - mBundle.getInt(SNIPPET_RANGE_LOWER_FIELD), - mBundle.getInt(SNIPPET_RANGE_UPPER_FIELD)); + if (mWindowRangeCached == null) { + mWindowRangeCached = new MatchRange( + mSnippetRangeStart, + mSnippetRangeEnd); } - return mWindowRange; + return mWindowRangeCached; } /** @@ -634,7 +740,7 @@ } private void checkSubmatchSupported() { - if (!mBundle.containsKey(SUBMATCH_RANGE_LOWER_FIELD)) { + if (mSubmatchRangeStart == -1) { throw new UnsupportedOperationException( "Submatch is not supported with this backend/Android API level " + "combination"); @@ -651,11 +757,29 @@ return result; } + /** + * Sets the {@link GenericDocument} for {@link MatchInfo}. + * + * {@link MatchInfo} lacks a constructor that populates {@link MatchInfo#mDocument} + * This provides the ability to set {@link MatchInfo#mDocument} + */ + void setDocument(@NonNull GenericDocument document) { + mDocument = document; + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + MatchInfoCreator.writeToParcel(this, dest, flags); + } + /** Builder for {@link MatchInfo} objects. */ public static final class Builder { private final String mPropertyPath; private MatchRange mExactMatchRange = new MatchRange(0, 0); - @Nullable private MatchRange mSubmatchRange; + int mSubmatchRangeStart = -1; + int mSubmatchRangeEnd = -1; private MatchRange mSnippetRange = new MatchRange(0, 0); /** @@ -675,6 +799,17 @@ mPropertyPath = Preconditions.checkNotNull(propertyPath); } + /** @exportToFramework:hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public Builder(@NonNull MatchInfo matchInfo) { + Preconditions.checkNotNull(matchInfo); + mPropertyPath = matchInfo.mPropertyPath; + mExactMatchRange = matchInfo.getExactMatchRange(); + mSubmatchRangeStart = matchInfo.mSubmatchRangeStart; + mSubmatchRangeEnd = matchInfo.mSubmatchRangeEnd; + mSnippetRange = matchInfo.getSnippetRange(); + } + /** Sets the exact {@link MatchRange} corresponding to the given entry. */ @CanIgnoreReturnValue @NonNull @@ -684,11 +819,15 @@ } - /** Sets the submatch {@link MatchRange} corresponding to the given entry. */ + /** + * Sets the start and end of a submatch {@link MatchRange} corresponding + * to the given entry. + */ @CanIgnoreReturnValue @NonNull public Builder setSubmatchRange(@NonNull MatchRange matchRange) { - mSubmatchRange = Preconditions.checkNotNull(matchRange); + mSubmatchRangeStart = matchRange.getStart(); + mSubmatchRangeEnd = matchRange.getEnd(); return this; } @@ -703,24 +842,14 @@ /** Constructs a new {@link MatchInfo}. */ @NonNull public MatchInfo build() { - Bundle bundle = new Bundle(); - bundle.putString(SearchResult.MatchInfo.PROPERTY_PATH_FIELD, mPropertyPath); - bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_LOWER_FIELD, mExactMatchRange.getStart()); - bundle.putInt(MatchInfo.EXACT_MATCH_RANGE_UPPER_FIELD, mExactMatchRange.getEnd()); - if (mSubmatchRange != null) { - // Only populate the submatch fields if it was actually set. - bundle.putInt(MatchInfo.SUBMATCH_RANGE_LOWER_FIELD, mSubmatchRange.getStart()); - } - - if (mSubmatchRange != null) { - // Only populate the submatch fields if it was actually set. - // Moved to separate block for Nullness Checker. - bundle.putInt(MatchInfo.SUBMATCH_RANGE_UPPER_FIELD, mSubmatchRange.getEnd()); - } - - bundle.putInt(MatchInfo.SNIPPET_RANGE_LOWER_FIELD, mSnippetRange.getStart()); - bundle.putInt(MatchInfo.SNIPPET_RANGE_UPPER_FIELD, mSnippetRange.getEnd()); - return new MatchInfo(bundle, /*document=*/ null); + return new MatchInfo( + mPropertyPath, + mExactMatchRange.getStart(), + mExactMatchRange.getEnd(), + mSubmatchRangeStart, + mSubmatchRangeEnd, + mSnippetRange.getStart(), + mSnippetRange.getEnd()); } } }
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
    index 568c2b6..d5d17a3 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResultPage.java
    
    @@ -16,14 +16,16 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
    -import androidx.core.util.Preconditions;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.SearchResultPageCreator;
     
    -import java.util.ArrayList;
     import java.util.Collections;
     import java.util.List;
     
    @@ -32,26 +34,29 @@
      * @exportToFramework:hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -public class SearchResultPage {
    -    public static final String RESULTS_FIELD = "results";
    -    public static final String NEXT_PAGE_TOKEN_FIELD = "nextPageToken";
    [email protected](creator = "SearchResultPageCreator")
    +public class SearchResultPage extends AbstractSafeParcelable {
    +    @NonNull public static final Parcelable.Creator CREATOR =
    +            new SearchResultPageCreator();
    +
    +    @Field(id = 1, getter = "getNextPageToken")
         private final long mNextPageToken;
    -
         @Nullable
    -    private List mResults;
    +    @Field(id = 2, getter = "getResults")
    +    private final List mResults;
     
    -    @NonNull
    -    private final Bundle mBundle;
    -
    -    public SearchResultPage(@NonNull Bundle bundle) {
    -        mBundle = Preconditions.checkNotNull(bundle);
    -        mNextPageToken = mBundle.getLong(NEXT_PAGE_TOKEN_FIELD);
    +    @Constructor
    +    public SearchResultPage(
    +            @Param(id = 1) long nextPageToken,
    +            @Param(id = 2) @Nullable List results) {
    +        mNextPageToken = nextPageToken;
    +        mResults = results;
         }
     
    -    /** Returns the {@link Bundle} of this class. */
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    +    /** Default constructor for {@link SearchResultPage}. */
    +    public SearchResultPage() {
    +        mNextPageToken = 0;
    +        mResults = Collections.emptyList();
         }
     
         /** Returns the Token to get next {@link SearchResultPage}. */
    @@ -61,19 +66,15 @@
     
         /** Returns all {@link androidx.appsearch.app.SearchResult}s of this page */
         @NonNull
    -    @SuppressWarnings("deprecation")
         public List getResults() {
             if (mResults == null) {
    -            ArrayList resultBundles = mBundle.getParcelableArrayList(RESULTS_FIELD);
    -            if (resultBundles == null) {
    -                mResults = Collections.emptyList();
    -            } else {
    -                mResults = new ArrayList<>(resultBundles.size());
    -                for (int i = 0; i < resultBundles.size(); i++) {
    -                    mResults.add(new SearchResult(resultBundles.get(i)));
    -                }
    -            }
    +            return Collections.emptyList();
             }
             return mResults;
         }
    +
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        SearchResultPageCreator.writeToParcel(this, dest, flags);
    +    }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
    index 9b86fc3..1b43e15 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
    
    @@ -18,6 +18,8 @@
     
     import android.annotation.SuppressLint;
     import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.IntDef;
     import androidx.annotation.IntRange;
    @@ -28,6 +30,11 @@
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
     import androidx.appsearch.annotation.Document;
     import androidx.appsearch.exceptions.AppSearchException;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.SearchSpecCreator;
     import androidx.appsearch.util.BundleUtil;
     import androidx.collection.ArrayMap;
     import androidx.collection.ArraySet;
    @@ -41,49 +48,119 @@
     import java.util.Collections;
     import java.util.List;
     import java.util.Map;
    +import java.util.Objects;
     import java.util.Set;
     
     /**
      * This class represents the specification logic for AppSearch. It can be used to set the type of
      * search, like prefix or exact only or apply filters to search for a specific schema type only etc.
      */
    -public final class SearchSpec {
    [email protected](creator = "SearchSpecCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class SearchSpec extends AbstractSafeParcelable {
    +
    +    /**  Creator class for {@link SearchSpec}. */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR =
    +            new SearchSpecCreator();
    +
         /**
          * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply
          * property paths to all results, excepting any types that have had their own, specific
          * property paths set.
    +     *
    +     * @deprecated use {@link #SCHEMA_TYPE_WILDCARD} instead.
          */
    +    @Deprecated
         public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     
         /**
          * Schema type to be used in {@link SearchSpec.Builder#addFilterProperties(String, Collection)}
    -     * to apply property paths to all results, excepting any types that have had their own, specific
    -     * property paths set.
    -     * @exportToFramework:hide
    +     * and {@link SearchSpec.Builder#addProjection} to apply property paths to all results,
    +     * excepting any types that have had their own, specific property paths set.
          */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES)
         public static final String SCHEMA_TYPE_WILDCARD = "*";
     
    -    static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
    -    static final String SCHEMA_FIELD = "schema";
    -    static final String NAMESPACE_FIELD = "namespace";
    -    static final String PROPERTY_FIELD = "property";
    -    static final String PACKAGE_NAME_FIELD = "packageName";
    -    static final String NUM_PER_PAGE_FIELD = "numPerPage";
    -    static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
    -    static final String ORDER_FIELD = "order";
    -    static final String SNIPPET_COUNT_FIELD = "snippetCount";
    -    static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty";
    -    static final String MAX_SNIPPET_FIELD = "maxSnippet";
    -    static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
    -    static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
    -    static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
    -    static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField";
    -    static final String JOIN_SPEC = "joinSpec";
    -    static final String ADVANCED_RANKING_EXPRESSION = "advancedRankingExpression";
    -    static final String ENABLED_FEATURES_FIELD = "enabledFeatures";
    +    @Field(id = 1, getter = "getTermMatch")
    +    private final int mTermMatchType;
     
    -    /** @exportToFramework:hide */
    +    @Field(id = 2, getter = "getFilterSchemas")
    +    private final List mSchemas;
    +
    +    @Field(id = 3, getter = "getFilterNamespaces")
    +    private final List mNamespaces;
    +
    +    @Field(id = 4)
    +    final Bundle mTypePropertyFilters;
    +
    +    @Field(id = 5, getter = "getFilterPackageNames")
    +    private final List mPackageNames;
    +
    +    @Field(id = 6, getter = "getResultCountPerPage")
    +    private final int mResultCountPerPage;
    +
    +    @Field(id = 7, getter = "getRankingStrategy")
    +    @RankingStrategy
    +    private final int mRankingStrategy;
    +
    +    @Field(id = 8, getter = "getOrder")
    +    @Order
    +    private final int mOrder;
    +
    +    @Field(id = 9, getter = "getSnippetCount")
    +    private final int mSnippetCount;
    +
    +    @Field(id = 10, getter = "getSnippetCountPerProperty")
    +    private final int mSnippetCountPerProperty;
    +
    +    @Field(id = 11, getter = "getMaxSnippetSize")
    +    private final int mMaxSnippetSize;
    +
    +    @Field(id = 12)
    +    final Bundle mProjectionTypePropertyMasks;
    +
    +    @Field(id = 13, getter = "getResultGroupingTypeFlags")
    +    @GroupingType
    +    private final int mResultGroupingTypeFlags;
    +
    +    @Field(id = 14, getter = "getResultGroupingLimit")
    +    private final int mGroupingLimit;
    +
    +    @Field(id = 15)
    +    final Bundle mTypePropertyWeightsField;
    +
    +    @Nullable
    +    @Field(id = 16, getter = "getJoinSpec")
    +    private final JoinSpec mJoinSpec;
    +
    +    @Field(id = 17, getter = "getAdvancedRankingExpression")
    +    private final String mAdvancedRankingExpression;
    +
    +    @Field(id = 18, getter = "getEnabledFeatures")
    +    private final List mEnabledFeatures;
    +
    +    @Field(id = 19, getter = "getSearchSourceLogTag")
    +    @Nullable private final String mSearchSourceLogTag;
    +
    +    @NonNull
    +    @Field(id = 20, getter = "getSearchEmbeddings")
    +    private final List mSearchEmbeddings;
    +
    +    @Field(id = 21, getter = "getDefaultEmbeddingSearchMetricType")
    +    private final int mDefaultEmbeddingSearchMetricType;
    +
    +    @NonNull
    +    @Field(id = 22, getter = "getInformationalRankingExpressions")
    +    private final List mInformationalRankingExpressions;
    +
    +    /**
    +     * Default number of documents per page.
    +     *
    +     * @exportToFramework:hide
    +     */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public static final int DEFAULT_NUM_PER_PAGE = 10;
     
    @@ -224,43 +301,112 @@
         /**
          * Results should be grouped together by schema type for the purpose of enforcing a limit on the
          * number of results returned per schema type.
    -     *
    -     * 
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.SEARCH_SPEC_GROUPING_TYPE_PER_SCHEMA)
    -    // @exportToFramework:endStrip()
    +    @FlaggedApi(Flags.FLAG_ENABLE_GROUPING_TYPE_PER_SCHEMA)
         public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2;
     
    -    private final Bundle mBundle;
    -
    -    /** @exportToFramework:hide */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public SearchSpec(@NonNull Bundle bundle) {
    -        Preconditions.checkNotNull(bundle);
    -        mBundle = bundle;
    -    }
    -
         /**
    -     * Returns the {@link Bundle} populated by this builder.
    +     * Type of scoring used to calculate similarity for embedding vectors. For details of each, see
    +     * comments above each value.
          *
          * @exportToFramework:hide
          */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    +    // NOTE: The integer values of these constants must match the proto enum constants in
    +    // {@link SearchSpecProto.EmbeddingQueryMetricType.Code}
    +    @RestrictTo(RestrictTo.Scope.LIBRARY)
    +    @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,
    +            @Param(id = 2) @NonNull List schemas,
    +            @Param(id = 3) @NonNull List namespaces,
    +            @Param(id = 4) @NonNull Bundle properties,
    +            @Param(id = 5) @NonNull List packageNames,
    +            @Param(id = 6) int resultCountPerPage,
    +            @Param(id = 7) @RankingStrategy int rankingStrategy,
    +            @Param(id = 8) @Order int order,
    +            @Param(id = 9) int snippetCount,
    +            @Param(id = 10) int snippetCountPerProperty,
    +            @Param(id = 11) int maxSnippetSize,
    +            @Param(id = 12) @NonNull Bundle projectionTypePropertyMasks,
    +            @Param(id = 13) int resultGroupingTypeFlags,
    +            @Param(id = 14) int groupingLimit,
    +            @Param(id = 15) @NonNull Bundle typePropertyWeightsField,
    +            @Param(id = 16) @Nullable JoinSpec joinSpec,
    +            @Param(id = 17) @NonNull String advancedRankingExpression,
    +            @Param(id = 18) @NonNull List enabledFeatures,
    +            @Param(id = 19) @Nullable String searchSourceLogTag,
    +            @Param(id = 20) @Nullable List searchEmbeddings,
    +            @Param(id = 21) int defaultEmbeddingSearchMetricType,
    +            @Param(id = 22) @Nullable List informationalRankingExpressions
    +    ) {
    +        mTermMatchType = termMatchType;
    +        mSchemas = Collections.unmodifiableList(Preconditions.checkNotNull(schemas));
    +        mNamespaces = Collections.unmodifiableList(Preconditions.checkNotNull(namespaces));
    +        mTypePropertyFilters = Preconditions.checkNotNull(properties);
    +        mPackageNames = Collections.unmodifiableList(Preconditions.checkNotNull(packageNames));
    +        mResultCountPerPage = resultCountPerPage;
    +        mRankingStrategy = rankingStrategy;
    +        mOrder = order;
    +        mSnippetCount = snippetCount;
    +        mSnippetCountPerProperty = snippetCountPerProperty;
    +        mMaxSnippetSize = maxSnippetSize;
    +        mProjectionTypePropertyMasks = Preconditions.checkNotNull(projectionTypePropertyMasks);
    +        mResultGroupingTypeFlags = resultGroupingTypeFlags;
    +        mGroupingLimit = groupingLimit;
    +        mTypePropertyWeightsField = Preconditions.checkNotNull(typePropertyWeightsField);
    +        mJoinSpec = joinSpec;
    +        mAdvancedRankingExpression = Preconditions.checkNotNull(advancedRankingExpression);
    +        mEnabledFeatures = Collections.unmodifiableList(
    +                Preconditions.checkNotNull(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. */
         @TermMatch
         public int getTermMatch() {
    -        return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1);
    +        return mTermMatchType;
         }
     
         /**
    @@ -270,11 +416,10 @@
          */
         @NonNull
         public List getFilterSchemas() {
    -        List schemas = mBundle.getStringArrayList(SCHEMA_FIELD);
    -        if (schemas == null) {
    +        if (mSchemas == null) {
                 return Collections.emptyList();
             }
    -        return Collections.unmodifiableList(schemas);
    +        return mSchemas;
         }
     
         /**
    @@ -284,19 +429,15 @@
          *
          * 

    Calling this function repeatedly is inefficient. Prefer to retain the Map returned * by this function, rather than calling it multiple times. - * - * @exportToFramework:hide */ @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Map> getFilterProperties() { - Bundle typePropertyPathsBundle = Preconditions.checkNotNull( - mBundle.getBundle(PROPERTY_FIELD)); - Set schemas = typePropertyPathsBundle.keySet(); + Set schemas = mTypePropertyFilters.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) { typePropertyPathsMap.put(schema, Preconditions.checkNotNull( - typePropertyPathsBundle.getStringArrayList(schema))); + mTypePropertyFilters.getStringArrayList(schema))); } return typePropertyPathsMap; } @@ -308,11 +449,10 @@ */ @NonNull public List getFilterNamespaces() { - List namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD); - if (namespaces == null) { + if (mNamespaces == null) { return Collections.emptyList(); } - return Collections.unmodifiableList(namespaces); + return mNamespaces; } /** @@ -324,45 +464,44 @@ */ @NonNull public List getFilterPackageNames() { - List packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD); - if (packageNames == null) { + if (mPackageNames == null) { return Collections.emptyList(); } - return Collections.unmodifiableList(packageNames); + return mPackageNames; } /** Returns the number of results per page in the result set. */ public int getResultCountPerPage() { - return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE); + return mResultCountPerPage; } /** Returns the ranking strategy. */ @RankingStrategy public int getRankingStrategy() { - return mBundle.getInt(RANKING_STRATEGY_FIELD); + return mRankingStrategy; } /** Returns the order of returned search results (descending or ascending). */ @Order public int getOrder() { - return mBundle.getInt(ORDER_FIELD); + return mOrder; } /** Returns how many documents to generate snippets for. */ public int getSnippetCount() { - return mBundle.getInt(SNIPPET_COUNT_FIELD); + return mSnippetCount; } /** * Returns how many matches for each property of a matching document to generate snippets for. */ public int getSnippetCountPerProperty() { - return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD); + return mSnippetCountPerProperty; } /** Returns the maximum size of a snippet in characters. */ public int getMaxSnippetSize() { - return mBundle.getInt(MAX_SNIPPET_FIELD); + return mMaxSnippetSize; } /** @@ -377,13 +516,12 @@ */ @NonNull public Map> getProjections() { - Bundle typePropertyPathsBundle = Preconditions.checkNotNull( - mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD)); - Set schemas = typePropertyPathsBundle.keySet(); + Set schemas = mProjectionTypePropertyMasks.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) { - typePropertyPathsMap.put(schema, Preconditions.checkNotNull( - typePropertyPathsBundle.getStringArrayList(schema))); + typePropertyPathsMap.put(schema, + Objects.requireNonNull( + mProjectionTypePropertyMasks.getStringArrayList(schema))); } return typePropertyPathsMap; } @@ -400,16 +538,19 @@ */ @NonNull public Map> getProjectionPaths() { - Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD); - Set schemas = typePropertyPathsBundle.keySet(); + Set schemas = mProjectionTypePropertyMasks.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) { - ArrayList propertyPathList = typePropertyPathsBundle.getStringArrayList(schema); - List copy = new ArrayList<>(propertyPathList.size()); - for (String p: propertyPathList) { - copy.add(new PropertyPath(p)); + ArrayList propertyPathList = mProjectionTypePropertyMasks.getStringArrayList( + schema); + if (propertyPathList != null) { + List 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; } @@ -425,18 +566,20 @@ */ @NonNull public Map> getPropertyWeights() { - Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD); - Set schemaTypes = typePropertyWeightsBundle.keySet(); + Set schemaTypes = mTypePropertyWeightsField.keySet(); Map> typePropertyWeightsMap = new ArrayMap<>( schemaTypes.size()); for (String schemaType : schemaTypes) { - Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType); - Set propertyPaths = propertyPathBundle.keySet(); - Map propertyPathWeights = new ArrayMap<>(propertyPaths.size()); - for (String propertyPath : propertyPaths) { - propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath)); + Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType); + if (propertyPathBundle != null) { + Set propertyPaths = propertyPathBundle.keySet(); + Map 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; } @@ -452,19 +595,22 @@ */ @NonNull public Map> getPropertyWeightPaths() { - Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD); - Set schemaTypes = typePropertyWeightsBundle.keySet(); + Set schemaTypes = mTypePropertyWeightsField.keySet(); Map> typePropertyWeightsMap = new ArrayMap<>( schemaTypes.size()); for (String schemaType : schemaTypes) { - Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType); - Set propertyPaths = propertyPathBundle.keySet(); - Map propertyPathWeights = new ArrayMap<>(propertyPaths.size()); - for (String propertyPath : propertyPaths) { - propertyPathWeights.put(new PropertyPath(propertyPath), - propertyPathBundle.getDouble(propertyPath)); + Bundle propertyPathBundle = mTypePropertyWeightsField.getBundle(schemaType); + if (propertyPathBundle != null) { + Set propertyPaths = propertyPathBundle.keySet(); + Map 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; } @@ -475,7 +621,7 @@ */ @GroupingType public int getResultGroupingTypeFlags() { - return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS); + return mResultGroupingTypeFlags; } /** @@ -485,7 +631,7 @@ * {@link Builder#setResultGrouping(int, int)} was not called. */ public int getResultGroupingLimit() { - return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE); + return mGroupingLimit; } /** @@ -493,11 +639,7 @@ */ @Nullable public JoinSpec getJoinSpec() { - Bundle joinSpec = mBundle.getBundle(JOIN_SPEC); - if (joinSpec == null) { - return null; - } - return new JoinSpec(joinSpec); + return mJoinSpec; } /** @@ -506,28 +648,104 @@ */ @NonNull public String getAdvancedRankingExpression() { - return mBundle.getString(ADVANCED_RANKING_EXPRESSION, ""); + return mAdvancedRankingExpression; + } + + + /** + * Gets a tag to indicate the source of this search, or {@code null} if + * {@link Builder#setSearchSourceLogTag(String)} was not called. + * + *

    Some AppSearch implementations may log a hash of this tag using statsd. This tag may be + * used for tracing performance issues and crashes to a component of an app. + * + *

    Call {@link Builder#setSearchSourceLogTag} and give a unique value if you want to + * distinguish this search scenario with other search scenarios during performance analysis. + * + *

    Under no circumstances will AppSearch log the raw String value using statsd, but it + * will be provided as-is to custom {@code AppSearchLogger} implementations you have + * registered in your app. + */ + @Nullable + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) + public String getSearchSourceLogTag() { + return mSearchSourceLogTag; + } + + /** + * Returns the list of {@link EmbeddingVector} for embedding search. + */ + @NonNull + @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG) + public List 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 getInformationalRankingExpressions() { + return mInformationalRankingExpressions; } /** * Returns whether the NUMERIC_SEARCH feature is enabled. */ public boolean isNumericSearchEnabled() { - return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH); + return mEnabledFeatures.contains(FeatureConstants.NUMERIC_SEARCH); } /** * Returns whether the VERBATIM_SEARCH feature is enabled. */ public boolean isVerbatimSearchEnabled() { - return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH); + return mEnabledFeatures.contains(FeatureConstants.VERBATIM_SEARCH); } /** * Returns whether the LIST_FILTER_QUERY_LANGUAGE feature is enabled. */ public boolean isListFilterQueryLanguageEnabled() { - return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE); + return mEnabledFeatures.contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE); + } + + /** + * Returns whether the LIST_FILTER_HAS_PROPERTY_FUNCTION feature is enabled. + */ + @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION) + public boolean isListFilterHasPropertyFunctionEnabled() { + 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); } /** @@ -539,21 +757,31 @@ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull public List getEnabledFeatures() { - return mBundle.getStringArrayList(ENABLED_FEATURES_FIELD); + return mEnabledFeatures; + } + + @Override + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + public void writeToParcel(@NonNull Parcel dest, int flags) { + SearchSpecCreator.writeToParcel(this, dest, flags); } /** Builder for {@link SearchSpec objects}. */ public static final class Builder { - private ArrayList mSchemas = new ArrayList<>(); - private ArrayList mNamespaces = new ArrayList<>(); + private List mSchemas = new ArrayList<>(); + private List mNamespaces = new ArrayList<>(); private Bundle mTypePropertyFilters = new Bundle(); - private ArrayList mPackageNames = new ArrayList<>(); + private List mPackageNames = new ArrayList<>(); private ArraySet mEnabledFeatures = new ArraySet<>(); private Bundle mProjectionTypePropertyMasks = new Bundle(); private Bundle mTypePropertyWeights = new Bundle(); + private List 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; @@ -561,10 +789,53 @@ @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 mInformationalRankingExpressions = new ArrayList<>(); + @Nullable private String mSearchSourceLogTag; private boolean mBuilt = false; + /** Constructs a new builder for {@link SearchSpec} objects. */ + public Builder() { + } + + /** @exportToFramework:hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public Builder(@NonNull SearchSpec searchSpec) { + Objects.requireNonNull(searchSpec); + mSchemas = new ArrayList<>(searchSpec.getFilterSchemas()); + mNamespaces = new ArrayList<>(searchSpec.getFilterNamespaces()); + for (Map.Entry> entry : + searchSpec.getFilterProperties().entrySet()) { + addFilterProperties(entry.getKey(), entry.getValue()); + } + mPackageNames = new ArrayList<>(searchSpec.getFilterPackageNames()); + mEnabledFeatures = new ArraySet<>(searchSpec.getEnabledFeatures()); + for (Map.Entry> entry : searchSpec.getProjections().entrySet()) { + addProjection(entry.getKey(), entry.getValue()); + } + for (Map.Entry> entry : + 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(); + mRankingStrategy = searchSpec.getRankingStrategy(); + mOrder = searchSpec.getOrder(); + mGroupingTypeFlags = searchSpec.getResultGroupingTypeFlags(); + mGroupingLimit = searchSpec.getResultGroupingLimit(); + mJoinSpec = searchSpec.getJoinSpec(); + mAdvancedRankingExpression = searchSpec.getAdvancedRankingExpression(); + mInformationalRankingExpressions = new ArrayList<>( + searchSpec.getInformationalRankingExpressions()); + mSearchSourceLogTag = searchSpec.getSearchSourceLogTag(); + } + /** * Sets how the query terms should match {@code TermMatchCode} in the index. * @@ -626,12 +897,13 @@ @SuppressLint("MissingGetterMatchingBuilder") @NonNull public Builder addFilterDocumentClasses( - @NonNull Collection> documentClasses) throws AppSearchException { + @NonNull Collection> documentClasses) + throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt(); List schemas = new ArrayList<>(documentClasses.size()); DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); - for (Class documentClass : documentClasses) { + for (java.lang.Class documentClass : documentClasses) { DocumentClassFactory factory = registry.getOrCreateFactory(documentClass); schemas.add(factory.getSchemaName()); } @@ -655,7 +927,7 @@ @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull - public Builder addFilterDocumentClasses(@NonNull Class... documentClasses) + public Builder addFilterDocumentClasses(@NonNull java.lang.Class... documentClasses) throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt(); @@ -685,17 +957,13 @@ * @param schema the {@link AppSearchSchema} that contains the target properties * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited * sequence of property names. - * - * @exportToFramework:hide */ - // TODO(b/296088047) unhide from framework when type property filters are made public. + @CanIgnoreReturnValue @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) - // @exportToFramework:endStrip() + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterProperties(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema); @@ -720,17 +988,14 @@ * * @param schema the {@link AppSearchSchema} that contains the target properties * @param propertyPaths The {@link PropertyPath} to search search over - * - * @exportToFramework:hide */ - // TODO(b/296088047) unhide from framework when type property filters are made public. @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - // @exportToFramework:startStrip() + // Getter method is getFilterProperties + @SuppressLint("MissingGetterMatchingBuilder") @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) - // @exportToFramework:endStrip() + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterPropertyPaths(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema); @@ -744,6 +1009,7 @@ // @exportToFramework:startStrip() + /** * Adds property paths for the specified type to the property filter of * {@link SearchSpec} Entry. Only returns documents that have matches under the specified @@ -758,11 +1024,10 @@ * */ @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) - public Builder addFilterProperties(@NonNull Class documentClass, + public Builder addFilterProperties(@NonNull java.lang.Class documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths); @@ -787,11 +1052,12 @@ * */ @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + // Getter method is getFilterProperties + @SuppressLint("MissingGetterMatchingBuilder") @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) - public Builder addFilterPropertyPaths(@NonNull Class documentClass, + public Builder addFilterPropertyPaths(@NonNull java.lang.Class documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths); @@ -966,6 +1232,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. + *

  • this.matchedSemanticScores(getSearchSpecEmbedding({embedding_index}), {metric}) + *

    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. + *

    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. * * *

    Some errors may occur when using advanced ranking. @@ -1011,11 +1290,9 @@ */ @CanIgnoreReturnValue @NonNull - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION) - // @exportToFramework:endStrip() public Builder setRankingStrategy(@NonNull String advancedRankingExpression) { Preconditions.checkStringNotEmpty(advancedRankingExpression); resetIfBuilt(); @@ -1025,6 +1302,88 @@ } /** + * 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. + * + *

    The syntax is exactly the same as specified in + * {@link SearchSpec.Builder#setRankingStrategy(String)}. + */ + @CanIgnoreReturnValue + @NonNull + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) + @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) + public Builder addInformationalRankingExpressions( + @NonNull String... informationalRankingExpressions) { + Preconditions.checkNotNull(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. + * + *

    The syntax is exactly the same as specified in + * {@link SearchSpec.Builder#setRankingStrategy(String)}. + */ + @CanIgnoreReturnValue + @NonNull + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_ADD_INFORMATIONAL_RANKING_EXPRESSIONS) + @FlaggedApi(Flags.FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS) + public Builder addInformationalRankingExpressions( + @NonNull Collection informationalRankingExpressions) { + Preconditions.checkNotNull(informationalRankingExpressions); + resetIfBuilt(); + mInformationalRankingExpressions.addAll(informationalRankingExpressions); + return this; + } + + /** + * Sets an optional log tag to indicate the source of this search. + * + *

    Some AppSearch implementations may log a hash of this tag using statsd. This tag + * may be used for tracing performance issues and crashes to a component of an app. + * + *

    Call this method and give a unique value if you want to distinguish this search + * scenario with other search scenarios during performance analysis. + * + *

    Under no circumstances will AppSearch log the raw String value using statsd, but it + * will be provided as-is to custom {@code AppSearchLogger} implementations you have + * registered in your app. + * + * @param searchSourceLogTag A String to indicate the source caller of this search. It is + * 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 + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_SET_SEARCH_SOURCE_LOG_TAG) + public Builder setSearchSourceLogTag(@NonNull String searchSourceLogTag) { + Preconditions.checkStringNotEmpty(searchSourceLogTag); + Preconditions.checkArgument(searchSourceLogTag.length() <= 100, + "The maximum supported tag length is 100. This tag is too long: " + + searchSourceLogTag.length()); + resetIfBuilt(); + mSearchSourceLogTag = searchSourceLogTag; + return this; + } + + /** * Sets the order of returned search results, the default is * {@link #ORDER_DESCENDING}, meaning that results with higher scores come first. * @@ -1143,9 +1502,8 @@ * results of that type will be retrieved. * *

    If property path is added for the - * {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, then those property paths will - * apply to all results, excepting any types that have their own, specific property paths - * set. + * {@link SearchSpec#SCHEMA_TYPE_WILDCARD}, then those property paths will apply to all + * results, excepting any types that have their own, specific property paths set. * *

    Suppose the following document is in the index. *

    {@code
    @@ -1226,7 +1584,8 @@
             @SuppressLint("MissingGetterMatchingBuilder")  // Projections available from getProjections
             @NonNull
             public SearchSpec.Builder addProjectionsForDocumentClass(
    -                @NonNull Class documentClass, @NonNull Collection propertyPaths)
    +                @NonNull java.lang.Class documentClass,
    +                @NonNull Collection propertyPaths)
                     throws AppSearchException {
                 Preconditions.checkNotNull(documentClass);
                 resetIfBuilt();
    @@ -1247,7 +1606,8 @@
             @SuppressLint("MissingGetterMatchingBuilder")  // Projections available from getProjections
             @NonNull
             public SearchSpec.Builder addProjectionPathsForDocumentClass(
    -                @NonNull Class documentClass, @NonNull Collection propertyPaths)
    +                @NonNull java.lang.Class documentClass,
    +                @NonNull Collection propertyPaths)
                     throws AppSearchException {
                 Preconditions.checkNotNull(documentClass);
                 resetIfBuilt();
    @@ -1319,12 +1679,10 @@
              *                            weight to set for that property.
              * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
              */
    -        // @exportToFramework:startStrip()
             @CanIgnoreReturnValue
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
    -        // @exportToFramework:endStrip()
             @NonNull
             public SearchSpec.Builder setPropertyWeights(@NonNull String schemaType,
                     @NonNull Map propertyPathWeights) {
    @@ -1354,12 +1712,10 @@
              *
              * @param joinSpec a specification on how to perform the Join operation.
              */
    -        // @exportToFramework:startStrip()
             @CanIgnoreReturnValue
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
    -        // @exportToFramework:endStrip()
             @NonNull
             public Builder setJoinSpec(@NonNull JoinSpec joinSpec) {
                 resetIfBuilt();
    @@ -1397,12 +1753,10 @@
              *                            weight to set for that property.
              * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
              */
    -        // @exportToFramework:startStrip()
             @CanIgnoreReturnValue
             @RequiresFeature(
                     enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                     name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
    -        // @exportToFramework:endStrip()
             @NonNull
             public SearchSpec.Builder setPropertyWeightPaths(@NonNull String schemaType,
                     @NonNull Map propertyPathWeights) {
    @@ -1460,7 +1814,7 @@
                     name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
             @NonNull
             public SearchSpec.Builder setPropertyWeightsForDocumentClass(
    -                @NonNull Class documentClass,
    +                @NonNull java.lang.Class documentClass,
                     @NonNull Map propertyPathWeights) throws AppSearchException {
                 Preconditions.checkNotNull(documentClass);
                 DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
    @@ -1508,7 +1862,7 @@
                     name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
             @NonNull
             public SearchSpec.Builder setPropertyWeightPathsForDocumentClass(
    -                @NonNull Class documentClass,
    +                @NonNull java.lang.Class documentClass,
                     @NonNull Map propertyPathWeights) throws AppSearchException {
                 Preconditions.checkNotNull(documentClass);
                 DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
    @@ -1518,6 +1872,72 @@
     // @exportToFramework:endStrip()
     
             /**
    +         * 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
    +        @RequiresFeature(
    +                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
    +                name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
    +        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
    +        public Builder addSearchEmbeddings(@NonNull EmbeddingVector... searchEmbeddings) {
    +            Preconditions.checkNotNull(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
    +        @RequiresFeature(
    +                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
    +                name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG)
    +        @FlaggedApi(Flags.FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG)
    +        public Builder addSearchEmbeddings(
    +                @NonNull Collection searchEmbeddings) {
    +            Preconditions.checkNotNull(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)}).
    +         *
    +         * 

    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 + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) + @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. @@ -1526,11 +1946,10 @@ * {@link AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric * querying features. */ - // @exportToFramework:startStrip() + @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.NUMERIC_SEARCH) - // @exportToFramework:endStrip() @NonNull public Builder setNumericSearchEnabled(boolean enabled) { modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled); @@ -1550,11 +1969,10 @@ *

    For example, The verbatim string operator '"foo/bar" OR baz' will ensure that * 'foo/bar' is treated as a single 'verbatim' token. */ - // @exportToFramework:startStrip() + @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.VERBATIM_SEARCH) - // @exportToFramework:endStrip() @NonNull public Builder setVerbatimSearchEnabled(boolean enabled) { modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled); @@ -1578,7 +1996,7 @@ *

    The newly added custom functions covered by this feature are: *

      *
    • createList(String...)
    • - *
    • termSearch(String, List)
    • + *
    • termSearch(String, {@code List})
    • *
    * *

    createList takes a variable number of strings and returns a list of strings. @@ -1590,11 +2008,10 @@ * for example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)" * could be rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))" */ - // @exportToFramework:startStrip() + @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.LIST_FILTER_QUERY_LANGUAGE) - // @exportToFramework:endStrip() @NonNull public Builder setListFilterQueryLanguageEnabled(boolean enabled) { modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled); @@ -1602,6 +2019,65 @@ } /** + * Sets the LIST_FILTER_HAS_PROPERTY_FUNCTION feature as enabled/disabled according to + * the enabled parameter. + * + * @param enabled Enables the feature if true, otherwise disables it + * + *

    If disabled, disallows the use of the "hasProperty" function. See + * {@link AppSearchSession#search} for more details about the function. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.LIST_FILTER_HAS_PROPERTY_FUNCTION) + @NonNull + @FlaggedApi(Flags.FLAG_ENABLE_LIST_FILTER_HAS_PROPERTY_FUNCTION) + public Builder setListFilterHasPropertyFunctionEnabled(boolean enabled) { + modifyEnabledFeature(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION, enabled); + return this; + } + + /** + * Sets the embedding search feature as enabled/disabled according to the enabled parameter. + * + *

    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 + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SCHEMA_EMBEDDING_PROPERTY_CONFIG) + @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 + * + *

    If disabled, disallows the use of the "tokenize" function. See + * {@link AppSearchSession#search} for more details about the function. + */ + @CanIgnoreReturnValue + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.LIST_FILTER_TOKENIZE_FUNCTION) + @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 @@ -1617,7 +2093,6 @@ */ @NonNull public SearchSpec build() { - Bundle bundle = new Bundle(); if (mJoinSpec != null) { if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE && mJoinSpec.getAggregationScoringStrategy() @@ -1626,59 +2101,26 @@ + "the nested JoinSpec, but ranking strategy is not " + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE"); } - bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle()); } else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) { throw new IllegalStateException("Attempting to rank based on joined documents, but " + "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 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."); - } - } - } - bundle.putStringArrayList(SCHEMA_FIELD, mSchemas); - bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters); - bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces); - bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames); - bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures)); - bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks); - bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage); - bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType); - bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount); - bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty); - bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize); - bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy); - bundle.putInt(ORDER_FIELD, mOrder); - bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags); - bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit); - bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights); - bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression); mBuilt = true; - return new SearchSpec(bundle); + return new SearchSpec(mTermMatchType, mSchemas, mNamespaces, + mTypePropertyFilters, mPackageNames, mResultCountPerPage, + mRankingStrategy, mOrder, mSnippetCount, mSnippetCountPerProperty, + mMaxSnippetSize, mProjectionTypePropertyMasks, mGroupingTypeFlags, + mGroupingLimit, mTypePropertyWeights, mJoinSpec, mAdvancedRankingExpression, + new ArrayList<>(mEnabledFeatures), mSearchSourceLogTag, mSearchEmbeddings, + mDefaultEmbeddingSearchMetricType, mInformationalRankingExpressions); } private void resetIfBuilt() { @@ -1689,6 +2131,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/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
    index 63fd696..c5171fd1 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionResult.java
    
    @@ -16,38 +16,40 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
    -import androidx.appsearch.util.BundleUtil;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionResultCreator;
     import androidx.core.util.Preconditions;
     
     /**
      * The result class of the {@link AppSearchSession#searchSuggestionAsync}.
      */
    -public final class SearchSuggestionResult {
    [email protected](creator = "SearchSuggestionResultCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class SearchSuggestionResult extends AbstractSafeParcelable {
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR =
    +            new SearchSuggestionResultCreator();
     
    -    private static final String SUGGESTED_RESULT_FIELD = "suggestedResult";
    -    private final Bundle mBundle;
    +    @Field(id = 1, getter = "getSuggestedResult")
    +    private final String mSuggestedResult;
         @Nullable
         private Integer mHashCode;
     
    -    SearchSuggestionResult(@NonNull Bundle bundle) {
    -        mBundle = Preconditions.checkNotNull(bundle);
    -    }
    -
    -    /**
    -     * Returns the {@link Bundle} populated by this builder.
    -     *
    -     * @exportToFramework:hide
    -     */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    +    @Constructor
    +    SearchSuggestionResult(@Param(id = 1) String suggestedResult) {
    +        mSuggestedResult = Preconditions.checkNotNull(suggestedResult);
         }
     
         /**
    @@ -60,7 +62,7 @@
          */
         @NonNull
         public String getSuggestedResult() {
    -        return Preconditions.checkNotNull(mBundle.getString(SUGGESTED_RESULT_FIELD));
    +        return mSuggestedResult;
         }
     
         @Override
    @@ -72,13 +74,13 @@
                 return false;
             }
             SearchSuggestionResult otherResult = (SearchSuggestionResult) other;
    -        return BundleUtil.deepEquals(this.mBundle, otherResult.mBundle);
    +        return mSuggestedResult.equals(otherResult.mSuggestedResult);
         }
     
         @Override
         public int hashCode() {
             if (mHashCode == null) {
    -            mHashCode = BundleUtil.deepHashCode(mBundle);
    +            mHashCode = mSuggestedResult.hashCode();
             }
             return mHashCode;
         }
    @@ -102,12 +104,17 @@
                 return this;
             }
     
    -        /** Build a {@link SearchSuggestionResult} object*/
    +        /** Build a {@link SearchSuggestionResult} object */
             @NonNull
             public SearchSuggestionResult build() {
    -            Bundle bundle = new Bundle();
    -            bundle.putString(SUGGESTED_RESULT_FIELD, mSuggestedResult);
    -            return new SearchSuggestionResult(bundle);
    +            return new SearchSuggestionResult(mSuggestedResult);
             }
         }
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        SearchSuggestionResultCreator.writeToParcel(this, dest, flags);
    +    }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
    index 4b87f9c..f2db304 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
    
    @@ -18,14 +18,22 @@
     
     import android.annotation.SuppressLint;
     import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.IntDef;
     import androidx.annotation.IntRange;
     import androidx.annotation.NonNull;
    +import androidx.annotation.RequiresFeature;
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
     import androidx.appsearch.annotation.Document;
     import androidx.appsearch.exceptions.AppSearchException;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.SearchSuggestionSpecCreator;
     import androidx.appsearch.util.BundleUtil;
     import androidx.collection.ArrayMap;
     import androidx.collection.ArraySet;
    @@ -47,24 +55,48 @@
      *
      * @see AppSearchSession#searchSuggestionAsync
      */
    -public final class SearchSuggestionSpec {
    -    static final String NAMESPACE_FIELD = "namespace";
    -    static final String SCHEMA_FIELD = "schema";
    -    static final String PROPERTY_FIELD = "property";
    -    static final String DOCUMENT_IDS_FIELD = "documentIds";
    -    static final String MAXIMUM_RESULT_COUNT_FIELD = "maximumResultCount";
    -    static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
    -    private final Bundle mBundle;
    [email protected](creator = "SearchSuggestionSpecCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class SearchSuggestionSpec extends AbstractSafeParcelable {
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull public static final Parcelable.Creator CREATOR =
    +            new SearchSuggestionSpecCreator();
    +    @Field(id = 1, getter = "getFilterNamespaces")
    +    private final List mFilterNamespaces;
    +    @Field(id = 2, getter = "getFilterSchemas")
    +    private final List 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)
    +    final Bundle mFilterDocumentIds;
    +    @Field(id = 5, getter = "getRankingStrategy")
    +    private final int mRankingStrategy;
    +    @Field(id = 6, getter = "getMaximumResultCount")
         private final int mMaximumResultCount;
     
         /** @exportToFramework:hide */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public SearchSuggestionSpec(@NonNull Bundle bundle) {
    -        Preconditions.checkNotNull(bundle);
    -        mBundle = bundle;
    -        mMaximumResultCount = bundle.getInt(MAXIMUM_RESULT_COUNT_FIELD);
    -        Preconditions.checkArgument(mMaximumResultCount >= 1,
    +    @Constructor
    +    public SearchSuggestionSpec(
    +            @Param(id = 1) @NonNull List filterNamespaces,
    +            @Param(id = 2) @NonNull List filterSchemas,
    +            @Param(id = 3) @NonNull Bundle filterProperties,
    +            @Param(id = 4) @NonNull Bundle filterDocumentIds,
    +            @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy,
    +            @Param(id = 6) int maximumResultCount) {
    +        Preconditions.checkArgument(maximumResultCount >= 1,
                     "MaximumResultCount must be positive.");
    +        mFilterNamespaces = Preconditions.checkNotNull(filterNamespaces);
    +        mFilterSchemas = Preconditions.checkNotNull(filterSchemas);
    +        mFilterProperties = Preconditions.checkNotNull(filterProperties);
    +        mFilterDocumentIds = Preconditions.checkNotNull(filterDocumentIds);
    +        mRankingStrategy = rankingStrategy;
    +        mMaximumResultCount = maximumResultCount;
         }
     
         /**
    @@ -111,17 +143,6 @@
         public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2;
     
         /**
    -     * Returns the {@link Bundle} populated by this builder.
    -     *
    -     * @exportToFramework:hide
    -     */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    -    }
    -
    -    /**
          * Returns the maximum number of wanted suggestion that will be returned in the result object.
          */
         public int getMaximumResultCount() {
    @@ -135,17 +156,16 @@
          */
         @NonNull
         public List getFilterNamespaces() {
    -        List namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD);
    -        if (namespaces == null) {
    +        if (mFilterNamespaces == null) {
                 return Collections.emptyList();
             }
    -        return Collections.unmodifiableList(namespaces);
    +        return Collections.unmodifiableList(mFilterNamespaces);
         }
     
         /** Returns the ranking strategy. */
         @SuggestionRankingStrategy
         public int getRankingStrategy() {
    -        return mBundle.getInt(RANKING_STRATEGY_FIELD);
    +        return mRankingStrategy;
         }
     
         /**
    @@ -155,11 +175,10 @@
          */
         @NonNull
         public List getFilterSchemas() {
    -        List schemaTypes = mBundle.getStringArrayList(SCHEMA_FIELD);
    -        if (schemaTypes == null) {
    +        if (mFilterSchemas == null) {
                 return Collections.emptyList();
             }
    -        return Collections.unmodifiableList(schemaTypes);
    +        return Collections.unmodifiableList(mFilterSchemas);
         }
     
         /**
    @@ -173,20 +192,15 @@
          *
          * 

    Calling this function repeatedly is inefficient. Prefer to retain the Map returned * by this function, rather than calling it multiple times. - * - * @exportToFramework:hide */ - // TODO(b/228240987) migrate this API when we support property restrict for multiple terms @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Map> getFilterProperties() { - Bundle typePropertyPathsBundle = Preconditions.checkNotNull( - mBundle.getBundle(PROPERTY_FIELD)); - Set schemas = typePropertyPathsBundle.keySet(); + Set schemas = mFilterProperties.keySet(); Map> typePropertyPathsMap = new ArrayMap<>(schemas.size()); for (String schema : schemas) { typePropertyPathsMap.put(schema, Preconditions.checkNotNull( - typePropertyPathsBundle.getStringArrayList(schema))); + mFilterProperties.getStringArrayList(schema))); } return typePropertyPathsMap; } @@ -205,13 +219,11 @@ */ @NonNull public Map> getFilterDocumentIds() { - Bundle documentIdsBundle = Preconditions.checkNotNull( - mBundle.getBundle(DOCUMENT_IDS_FIELD)); - Set namespaces = documentIdsBundle.keySet(); + Set namespaces = mFilterDocumentIds.keySet(); Map> documentIdsMap = new ArrayMap<>(namespaces.size()); for (String namespace : namespaces) { documentIdsMap.put(namespace, Preconditions.checkNotNull( - documentIdsBundle.getStringArrayList(namespace))); + mFilterDocumentIds.getStringArrayList(namespace))); } return documentIdsMap; } @@ -328,7 +340,7 @@ @SuppressLint("MissingGetterMatchingBuilder") @CanIgnoreReturnValue @NonNull - public Builder addFilterDocumentClasses(@NonNull Class... documentClasses) + public Builder addFilterDocumentClasses(@NonNull java.lang.Class... documentClasses) throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt(); @@ -353,12 +365,13 @@ @CanIgnoreReturnValue @NonNull public Builder addFilterDocumentClasses( - @NonNull Collection> documentClasses) throws AppSearchException { + @NonNull Collection> documentClasses) + throws AppSearchException { Preconditions.checkNotNull(documentClasses); resetIfBuilt(); List schemas = new ArrayList<>(documentClasses.size()); DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); - for (Class documentClass : documentClasses) { + for (java.lang.Class documentClass : documentClasses) { DocumentClassFactory factory = registry.getOrCreateFactory(documentClass); schemas.add(factory.getSchemaName()); } @@ -385,11 +398,13 @@ * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited * sequence of property names indicating which property in the * document these snippets correspond to. - * @exportToFramework:hide */ - // TODO(b/228240987) migrate this API when we support property restrict for multiple terms + @CanIgnoreReturnValue @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterProperties(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema); @@ -418,12 +433,15 @@ * * @param schema the {@link AppSearchSchema} that contains the target properties * @param propertyPaths The {@link PropertyPath} to search suggestion over - * - * @exportToFramework:hide */ - // TODO(b/228240987) migrate this API when we support property restrict for multiple terms + @CanIgnoreReturnValue @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + // Getter method is getFilterProperties + @SuppressLint("MissingGetterMatchingBuilder") + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) + @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) public Builder addFilterPropertyPaths(@NonNull String schema, @NonNull Collection propertyPaths) { Preconditions.checkNotNull(schema); @@ -453,12 +471,12 @@ * @param propertyPaths The String version of {@link PropertyPath}. A * {@code dot-delimited sequence of property names indicating which property in the * document these snippets correspond to. - * @exportToFramework:hide */ - // TODO(b/228240987) migrate this API when we support property restrict for multiple terms @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public Builder addFilterProperties(@NonNull Class documentClass, + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) + public Builder addFilterProperties(@NonNull java.lang.Class documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths); @@ -484,12 +502,14 @@ * * @param documentClass class annotated with {@link Document}. * @param propertyPaths The {@link PropertyPath} to search suggestion over - * @exportToFramework:hide */ - // TODO(b/228240987) migrate this API when we support property restrict for multiple terms @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public Builder addFilterPropertyPaths(@NonNull Class documentClass, + // Getter method is getFilterProperties + @SuppressLint("MissingGetterMatchingBuilder") + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SEARCH_SPEC_ADD_FILTER_PROPERTIES) + public Builder addFilterPropertyPaths(@NonNull java.lang.Class documentClass, @NonNull Collection propertyPaths) throws AppSearchException { Preconditions.checkNotNull(documentClass); Preconditions.checkNotNull(propertyPaths); @@ -540,7 +560,6 @@ /** Constructs a new {@link SearchSpec} from the contents of this builder. */ @NonNull public SearchSuggestionSpec build() { - Bundle bundle = new Bundle(); if (!mSchemas.isEmpty()) { Set schemaFilter = new ArraySet<>(mSchemas); for (String schema : mTypePropertyFilters.keySet()) { @@ -561,14 +580,14 @@ } } } - bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces); - bundle.putStringArrayList(SCHEMA_FIELD, mSchemas); - bundle.putBundle(PROPERTY_FIELD, mTypePropertyFilters); - bundle.putBundle(DOCUMENT_IDS_FIELD, mDocumentIds); - bundle.putInt(MAXIMUM_RESULT_COUNT_FIELD, mTotalResultCount); - bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy); mBuilt = true; - return new SearchSuggestionSpec(bundle); + return new SearchSuggestionSpec( + mNamespaces, + mSchemas, + mTypePropertyFilters, + mDocumentIds, + mRankingStrategy, + mTotalResultCount); } private void resetIfBuilt() { @@ -581,4 +600,11 @@ } } } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + SearchSuggestionSpecCreator.writeToParcel(this, dest, flags); + } }

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
    index 6cc0e85..3cabaab 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
    
    @@ -21,10 +21,13 @@
     import androidx.annotation.IntDef;
     import androidx.annotation.IntRange;
     import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
     import androidx.annotation.RequiresFeature;
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
     import androidx.appsearch.exceptions.AppSearchException;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
     import androidx.collection.ArrayMap;
     import androidx.collection.ArraySet;
     import androidx.core.util.Preconditions;
    @@ -100,13 +103,13 @@
                 READ_EXTERNAL_STORAGE,
                 READ_HOME_APP_SEARCH_DATA,
                 READ_ASSISTANT_APP_SEARCH_DATA,
    +            ENTERPRISE_ACCESS,
    +            MANAGED_PROFILE_CONTACTS_ACCESS,
         })
         @Retention(RetentionPolicy.SOURCE)
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public @interface AppSearchSupportedPermission {}
     
    @@ -114,72 +117,85 @@
          * The {@link android.Manifest.permission#READ_SMS} AppSearch supported in
          * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         public static final int READ_SMS = 1;
     
         /**
          * The {@link android.Manifest.permission#READ_CALENDAR} AppSearch supported in
          * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         public static final int READ_CALENDAR = 2;
     
         /**
          * The {@link android.Manifest.permission#READ_CONTACTS} AppSearch supported in
          * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         public static final int READ_CONTACTS = 3;
     
         /**
          * The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} AppSearch supported in
          * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         public static final int READ_EXTERNAL_STORAGE = 4;
     
         /**
          * The {@link android.Manifest.permission#READ_HOME_APP_SEARCH_DATA} AppSearch supported in
          * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         public static final int READ_HOME_APP_SEARCH_DATA = 5;
     
         /**
          * The {@link android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA} AppSearch supported in
          * {@link SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
          */
    -    // @exportToFramework:startStrip()
         @RequiresFeature(
                 enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
                 name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)
    -    // @exportToFramework:endStrip()
         public static final int READ_ASSISTANT_APP_SEARCH_DATA = 6;
     
    +    /**
    +     * A schema must have this permission set through {@link
    +     * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} to be visible to an
    +     * {@link EnterpriseGlobalSearchSession}. A call from a regular {@link GlobalSearchSession} will
    +     * not count as having this permission.
    +     *
    +     * @exportToFramework:hide
    +     */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    public static final int ENTERPRISE_ACCESS = 7;
    +
    +    /**
    +     * A schema with this permission set through {@link
    +     * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility} requires the caller
    +     * to have managed profile contacts access from {@link android.app.admin.DevicePolicyManager} to
    +     * be visible. This permission indicates that the protected schema may expose managed profile
    +     * data for contacts search.
    +     *
    +     * @exportToFramework:hide
    +     */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    public static final int MANAGED_PROFILE_CONTACTS_ACCESS = 8;
    +
         private final Set mSchemas;
         private final Set mSchemasNotDisplayedBySystem;
         private final Map> mSchemasVisibleToPackages;
         private final Map>> mSchemasVisibleToPermissions;
    +    private final Map mPubliclyVisibleSchemas;
    +    private final Map> mSchemasVisibleToConfigs;
         private final Map mMigrators;
         private final boolean mForceOverride;
         private final int mVersion;
    @@ -188,6 +204,8 @@
                 @NonNull Set schemasNotDisplayedBySystem,
                 @NonNull Map> schemasVisibleToPackages,
                 @NonNull Map>> schemasVisibleToPermissions,
    +            @NonNull Map publiclyVisibleSchemas,
    +            @NonNull Map> schemasVisibleToConfigs,
                 @NonNull Map migrators,
                 boolean forceOverride,
                 int version) {
    @@ -195,6 +213,8 @@
             mSchemasNotDisplayedBySystem = Preconditions.checkNotNull(schemasNotDisplayedBySystem);
             mSchemasVisibleToPackages = Preconditions.checkNotNull(schemasVisibleToPackages);
             mSchemasVisibleToPermissions = Preconditions.checkNotNull(schemasVisibleToPermissions);
    +        mPubliclyVisibleSchemas = Preconditions.checkNotNull(publiclyVisibleSchemas);
    +        mSchemasVisibleToConfigs = Preconditions.checkNotNull(schemasVisibleToConfigs);
             mMigrators = Preconditions.checkNotNull(migrators);
             mForceOverride = forceOverride;
             mVersion = version;
    @@ -258,12 +278,41 @@
          *         {@link SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and
          *         {@link SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
          */
    +    // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden
         @NonNull
         public Map>> getRequiredPermissionsForSchemaTypeVisibility() {
             return deepCopy(mSchemasVisibleToPermissions);
         }
     
         /**
    +     * Returns a mapping of publicly visible schemas to the {@link PackageIdentifier} specifying
    +     * the package the schemas are from.
    +     */
    +    @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA)
    +    @NonNull
    +    public Map getPubliclyVisibleSchemas() {
    +        return Collections.unmodifiableMap(mPubliclyVisibleSchemas);
    +    }
    +
    +    /**
    +     * Returns a mapping of schema types to the set of {@link SchemaVisibilityConfig} that have
    +     * access to that schema type.
    +     *
    +     * 

    It’s inefficient to call this method repeatedly. + * @see SetSchemaRequest.Builder#addSchemaTypeVisibleToConfig + */ + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + @NonNull + public Map> getSchemasVisibleToConfigs() { + Map> copy = new ArrayMap<>(); + for (Map.Entry> entry : + mSchemasVisibleToConfigs.entrySet()) { + copy.put(entry.getKey(), new ArraySet<>(entry.getValue())); + } + return copy; + } + + /** * Returns the map of {@link Migrator}, the key will be the schema type of the * {@link Migrator} associated with. */ @@ -307,6 +356,9 @@ private ArrayMap> mSchemasVisibleToPackages = new ArrayMap<>(); private ArrayMap>> mSchemasVisibleToPermissions = new ArrayMap<>(); + private ArrayMap mPubliclyVisibleSchemas = new ArrayMap<>(); + private ArrayMap> mSchemaVisibleToConfigs = + new ArrayMap<>(); private ArrayMap mMigrators = new ArrayMap<>(); private boolean mForceOverride = false; private int mVersion = DEFAULT_VERSION; @@ -451,10 +503,16 @@ *

    You can call this method to add multiple permission combinations, and the querier * will have access if they holds ANY of the combinations. * - *

    The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR}, + *

    The supported Permissions are {@link #READ_SMS}, {@link #READ_CALENDAR}, * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE}, * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}. * + *

    The relationship between permissions added in this method and package visibility + * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access + * the schema if they match ANY requirements. If you want to set "AND" requirements like + * a caller must hold required permissions AND it is a specified package, please use + * {@link #addSchemaTypeVisibleToConfig}. + * * @see android.Manifest.permission#READ_SMS * @see android.Manifest.permission#READ_CALENDAR * @see android.Manifest.permission#READ_CONTACTS @@ -467,14 +525,13 @@ * schema. * @throws IllegalArgumentException – if input unsupported permission. */ + // TODO(b/237388235): add enterprise permissions to javadocs after they're unhidden // Merged list available from getRequiredPermissionsForSchemaTypeVisibility @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") - // @exportToFramework:startStrip() @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) - // @exportToFramework:endStrip() @NonNull public Builder addRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType, @AppSearchSupportedPermission @NonNull Set permissions) { @@ -482,7 +539,7 @@ Preconditions.checkNotNull(permissions); for (int permission : permissions) { Preconditions.checkArgumentInRange(permission, READ_SMS, - READ_ASSISTANT_APP_SEARCH_DATA, "permission"); + MANAGED_PROFILE_CONTACTS_ACCESS, "permission"); } resetIfBuilt(); Set> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType); @@ -495,12 +552,10 @@ } /** Clears all required permissions combinations for the given schema type. */ - // @exportToFramework:startStrip() @CanIgnoreReturnValue @RequiresFeature( enforcement = "androidx.appsearch.app.Features#isFeatureSupported", name = Features.ADD_PERMISSIONS_AND_GET_VISIBILITY) - // @exportToFramework:endStrip() @NonNull public Builder clearRequiredPermissionsForSchemaTypeVisibility(@NonNull String schemaType) { Preconditions.checkNotNull(schemaType); @@ -525,6 +580,12 @@ * *

    By default, data sharing between applications is disabled. * + *

    The relationship between permissions added in this method and package visibility + * setting {@link #setSchemaTypeVisibilityForPackage} is "OR". The caller could access + * the schema if they match ANY requirements. If you want to set "AND" requirements like + * a caller must hold required permissions AND it is a specified package, please use + * {@link #addSchemaTypeVisibleToConfig}. + * * @param schemaType The schema type to set visibility on. * @param visible Whether the {@code schemaType} will be visible or not. * @param packageIdentifier Represents the package that will be granted visibility. @@ -564,6 +625,132 @@ } /** + * Specify that the schema should be publicly available, to packages which already have + * visibility to {@code packageIdentifier}. This visibility is determined by the result of + * {@link android.content.pm.PackageManager#canPackageQuery}. + * + *

    It is possible for the packageIdentifier parameter to be different from the + * package performing the indexing. This might happen in the case of an on-device indexer + * processing information about various packages. The visibility will be the same + * regardless of which package indexes the document, as the visibility is based on the + * packageIdentifier parameter. + * + *

    If this is called repeatedly with the same schema, the {@link PackageIdentifier} in + * the last call will be used as the "from" package for that schema. + * + *

    Calling this with packageIdentifier set to null is valid, and will remove public + * visibility for the schema. + * + * @param schema the schema to make publicly accessible. + * @param packageIdentifier if an app can see this package via + * PackageManager#canPackageQuery, it will be able to see the + * documents of type {@code schema}. + */ + // Merged list available from getPubliclyVisibleSchemas + @CanIgnoreReturnValue + @SuppressLint("MissingGetterMatchingBuilder") + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) + @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA) + @NonNull + public Builder setPubliclyVisibleSchema(@NonNull String schema, + @Nullable PackageIdentifier packageIdentifier) { + Preconditions.checkNotNull(schema); + resetIfBuilt(); + + // If the package identifier is null or empty we clear public visibility + if (packageIdentifier == null || packageIdentifier.getPackageName().isEmpty()) { + mPubliclyVisibleSchemas.remove(schema); + return this; + } + + mPubliclyVisibleSchemas.put(schema, packageIdentifier); + return this; + } + +// @exportToFramework:startStrip() + /** + * Specify that the schema should be publicly available, to packages which already have + * visibility to {@code packageIdentifier}. + * + * @param documentClass the document to make publicly accessible. + * @param packageIdentifier if an app can see this package via + * PackageManager#canPackageQuery, it will be able to see the + * documents of type {@code documentClass}. + * @see SetSchemaRequest.Builder#setPubliclyVisibleSchema + */ + // Merged list available from getPubliclyVisibleSchemas + @CanIgnoreReturnValue + @SuppressLint("MissingGetterMatchingBuilder") + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SET_SCHEMA_REQUEST_SET_PUBLICLY_VISIBLE) + @FlaggedApi(Flags.FLAG_ENABLE_SET_PUBLICLY_VISIBLE_SCHEMA) + @NonNull + public Builder setPubliclyVisibleDocumentClass(@NonNull Class documentClass, + @Nullable PackageIdentifier packageIdentifier) throws AppSearchException { + Preconditions.checkNotNull(documentClass); + resetIfBuilt(); + DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); + DocumentClassFactory factory = registry.getOrCreateFactory(documentClass); + return setPubliclyVisibleSchema(factory.getSchemaName(), packageIdentifier); + } +// @exportToFramework:endStrip() + + /** + * Sets the documents from the provided {@code schemaType} can be read by the caller if they + * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}. + * + *

    The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A + * caller must match ALL requirements to access the schema. For example, a caller must hold + * required permissions AND it is a specified package. + * + *

    You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s, + * and the querier will have access if they match ANY of the + * {@link SchemaVisibilityConfig}. + * + * @param schemaType The schema type to set visibility on. + * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all requirements + * 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) + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) + @NonNull + public Builder addSchemaTypeVisibleToConfig(@NonNull String schemaType, + @NonNull SchemaVisibilityConfig schemaVisibilityConfig) { + Preconditions.checkNotNull(schemaType); + Preconditions.checkNotNull(schemaVisibilityConfig); + resetIfBuilt(); + Set visibleToConfigs = mSchemaVisibleToConfigs.get(schemaType); + if (visibleToConfigs == null) { + visibleToConfigs = new ArraySet<>(); + mSchemaVisibleToConfigs.put(schemaType, visibleToConfigs); + } + visibleToConfigs.add(schemaVisibilityConfig); + return this; + } + + /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */ + @CanIgnoreReturnValue + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) + @NonNull + public Builder clearSchemaTypeVisibleToConfigs(@NonNull String schemaType) { + Preconditions.checkNotNull(schemaType); + resetIfBuilt(); + mSchemaVisibleToConfigs.remove(schemaType); + return this; + } + + /** * Sets the {@link Migrator} associated with the given SchemaType. * *

    The {@link Migrator} migrates all {@link GenericDocument}s under given schema type @@ -684,6 +871,12 @@ * *

    By default, app data sharing between applications is disabled. * + *

    The relationship between visible packages added in this method and permission + * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The + * caller could access the schema if they match ANY requirements. If you want to set + * "AND" requirements like a caller must hold required permissions AND it is a specified + * package, please use {@link #addSchemaTypeVisibleToConfig}. + * *

    Merged list available from {@link #getSchemasVisibleToPackages()}. * * @param documentClass The {@link androidx.appsearch.annotation.Document} class to set @@ -721,6 +914,12 @@ * {@link #READ_CONTACTS}, {@link #READ_EXTERNAL_STORAGE}, * {@link #READ_HOME_APP_SEARCH_DATA} and {@link #READ_ASSISTANT_APP_SEARCH_DATA}. * + *

    The relationship between visible packages added in this method and permission + * visibility setting {@link #addRequiredPermissionsForSchemaTypeVisibility} is "OR". The + * caller could access the schema if they match ANY requirements. If you want to set + * "AND" requirements like a caller must hold required permissions AND it is a specified + * package, please use {@link #addSchemaTypeVisibleToConfig}. + * *

    Merged map available from {@link #getRequiredPermissionsForSchemaTypeVisibility()}. * @see android.Manifest.permission#READ_SMS * @see android.Manifest.permission#READ_CALENDAR @@ -768,6 +967,58 @@ DocumentClassFactory factory = registry.getOrCreateFactory(documentClass); return clearRequiredPermissionsForSchemaTypeVisibility(factory.getSchemaName()); } + + /** + * Sets the documents from the provided {@code schemaType} can be read by the caller if they + * match the ALL visibility requirements set in {@link SchemaVisibilityConfig}. + * + *

    The requirements in a {@link SchemaVisibilityConfig} is "AND" relationship. A + * caller must match ALL requirements to access the schema. For example, a caller must hold + * required permissions AND it is a specified package. + * + *

    You can call this method repeatedly to add multiple {@link SchemaVisibilityConfig}s, + * and the querier will have access if they match ANY of the {@link SchemaVisibilityConfig}. + * + * @param documentClass A class annotated with + * {@link androidx.appsearch.annotation.Document}, the + * visibility of which will be configured + * @param schemaVisibilityConfig The {@link SchemaVisibilityConfig} holds all + * requirements that a call must to match to access the + * schema. + */ + // Merged list available from getSchemasVisibleToConfigs + @SuppressLint("MissingGetterMatchingBuilder") + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + @NonNull + public Builder addDocumentClassVisibleToConfig( + @NonNull Class documentClass, + @NonNull SchemaVisibilityConfig schemaVisibilityConfig) + throws AppSearchException { + Preconditions.checkNotNull(documentClass); + resetIfBuilt(); + DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); + DocumentClassFactory factory = registry.getOrCreateFactory(documentClass); + return addSchemaTypeVisibleToConfig(factory.getSchemaName(), + schemaVisibilityConfig); + } + + /** Clears all visible to {@link SchemaVisibilityConfig} for the given schema type. */ + @RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.SET_SCHEMA_REQUEST_ADD_SCHEMA_TYPE_VISIBLE_TO_CONFIG) + @FlaggedApi(Flags.FLAG_ENABLE_SET_SCHEMA_VISIBLE_TO_CONFIGS) + @NonNull + public Builder clearDocumentClassVisibleToConfigs( + @NonNull Class documentClass) throws AppSearchException { + Preconditions.checkNotNull(documentClass); + resetIfBuilt(); + DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance(); + DocumentClassFactory factory = registry.getOrCreateFactory(documentClass); + return clearSchemaTypeVisibleToConfigs(factory.getSchemaName()); + } // @exportToFramework:endStrip() /** @@ -841,6 +1092,8 @@ Set referencedSchemas = new ArraySet<>(mSchemasNotDisplayedBySystem); referencedSchemas.addAll(mSchemasVisibleToPackages.keySet()); referencedSchemas.addAll(mSchemasVisibleToPermissions.keySet()); + referencedSchemas.addAll(mPubliclyVisibleSchemas.keySet()); + referencedSchemas.addAll(mSchemaVisibleToConfigs.keySet()); for (AppSearchSchema schema : mSchemas) { referencedSchemas.remove(schema.getSchemaType()); @@ -861,6 +1114,8 @@ mSchemasNotDisplayedBySystem, mSchemasVisibleToPackages, mSchemasVisibleToPermissions, + mPubliclyVisibleSchemas, + mSchemaVisibleToConfigs, mMigrators, mForceOverride, mVersion); @@ -876,8 +1131,18 @@ } mSchemasVisibleToPackages = schemasVisibleToPackages; + mPubliclyVisibleSchemas = new ArrayMap<>(mPubliclyVisibleSchemas); + mSchemasVisibleToPermissions = deepCopy(mSchemasVisibleToPermissions); + ArrayMap> schemaVisibleToConfigs = + new ArrayMap<>(mSchemaVisibleToConfigs.size()); + for (Map.Entry> entry : + mSchemaVisibleToConfigs.entrySet()) { + schemaVisibleToConfigs.put(entry.getKey(), new ArraySet<>(entry.getValue())); + } + mSchemaVisibleToConfigs = schemaVisibleToConfigs; + mSchemas = new ArraySet<>(mSchemas); mSchemasNotDisplayedBySystem = new ArraySet<>(mSchemasNotDisplayedBySystem); mMigrators = new ArrayMap<>(mMigrators); @@ -886,8 +1151,8 @@ } } - static ArrayMap>> deepCopy(@NonNull Map - Set>> original) { + private static ArrayMap>> deepCopy( + @NonNull Map>> original) { ArrayMap>> copy = new ArrayMap<>(original.size()); for (Map.Entry>> entry : original.entrySet()) { Set> valueCopy = new ArraySet<>();

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
    index 07d087a..85866fd 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaResponse.java
    
    @@ -16,12 +16,19 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.MigrationFailureCreator;
    +import androidx.appsearch.safeparcel.stub.StubCreators.SetSchemaResponseCreator;
     import androidx.collection.ArraySet;
     import androidx.core.util.Preconditions;
     
    @@ -32,57 +39,70 @@
     import java.util.Set;
     
     /** The response class of {@link AppSearchSession#setSchemaAsync} */
    -public class SetSchemaResponse {
    [email protected](creator = "SetSchemaResponseCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class SetSchemaResponse extends AbstractSafeParcelable {
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull public static final Parcelable.Creator CREATOR =
    +            new SetSchemaResponseCreator();
     
    -    private static final String DELETED_TYPES_FIELD = "deletedTypes";
    -    private static final String INCOMPATIBLE_TYPES_FIELD = "incompatibleTypes";
    -    private static final String MIGRATED_TYPES_FIELD = "migratedTypes";
    +    @Field(id = 1)
    +    final List mDeletedTypes;
    +    @Field(id = 2)
    +    final List mIncompatibleTypes;
    +    @Field(id = 3)
    +    final List mMigratedTypes;
     
    -    private final Bundle mBundle;
         /**
    -     * The migrationFailures won't be saved in the bundle. Since:
    +     * The migrationFailures won't be saved as a SafeParcelable field. Since:
          * 
      *
    • {@link MigrationFailure} is generated in {@link AppSearchSession} which will be - * the SDK side in platform. We don't need to pass it from service side via binder. - *
    • Translate multiple {@link MigrationFailure}s to bundles in {@link Builder} and then - * back in constructor will be a huge waste. + * the SDK side in platform. We don't need to pass it from service side via binder as + * a part of {@link SetSchemaResponse}. + *
    • Writing multiple {@link MigrationFailure}s to SafeParcelable in {@link Builder} and + * then back in constructor will be a huge waste. *
    */ private final List mMigrationFailures; - /** Cache of the inflated deleted schema types. Comes from inflating mBundles at first use. */ - @Nullable - private Set mDeletedTypes; + /** Cache of the inflated deleted schema types. Comes from inflating mDeletedTypes at first use + */ + @Nullable private Set mDeletedTypesCached; - /** Cache of the inflated migrated schema types. Comes from inflating mBundles at first use. */ - @Nullable - private Set mMigratedTypes; + /** Cache of the inflated migrated schema types. Comes from inflating mMigratedTypes at first + * use. + */ + @Nullable private Set mMigratedTypesCached; /** - * Cache of the inflated incompatible schema types. Comes from inflating mBundles at first use. + * Cache of the inflated incompatible schema types. Comes from inflating mIncompatibleTypes at + * first use. */ - @Nullable - private Set mIncompatibleTypes; + @Nullable private Set mIncompatibleTypesCached; - SetSchemaResponse(@NonNull Bundle bundle, @NonNull List migrationFailures) { - mBundle = Preconditions.checkNotNull(bundle); + @Constructor + SetSchemaResponse( + @Param(id = 1) @NonNull List deletedTypes, + @Param(id = 2) @NonNull List incompatibleTypes, + @Param(id = 3) @NonNull List migratedTypes) { + mDeletedTypes = deletedTypes; + mIncompatibleTypes = incompatibleTypes; + mMigratedTypes = migratedTypes; + mMigrationFailures = Collections.emptyList(); + } + + SetSchemaResponse( + @NonNull List deletedTypes, + @NonNull List incompatibleTypes, + @NonNull List migratedTypes, + @NonNull List migrationFailures) { + mDeletedTypes = deletedTypes; + mIncompatibleTypes = incompatibleTypes; + mMigratedTypes = migratedTypes; mMigrationFailures = Preconditions.checkNotNull(migrationFailures); } - SetSchemaResponse(@NonNull Bundle bundle) { - this(bundle, /*migrationFailures=*/ Collections.emptyList()); - } - - /** - * Returns the {@link Bundle} populated by this builder. - * @exportToFramework:hide - */ - @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public Bundle getBundle() { - return mBundle; - } - /** * Returns a {@link List} of all failed {@link MigrationFailure}. * @@ -109,11 +129,10 @@ */ @NonNull public Set getDeletedTypes() { - if (mDeletedTypes == null) { - mDeletedTypes = new ArraySet<>( - Preconditions.checkNotNull(mBundle.getStringArrayList(DELETED_TYPES_FIELD))); + if (mDeletedTypesCached == null) { + mDeletedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mDeletedTypes)); } - return Collections.unmodifiableSet(mDeletedTypes); + return Collections.unmodifiableSet(mDeletedTypesCached); } /** @@ -131,11 +150,10 @@ */ @NonNull public Set getMigratedTypes() { - if (mMigratedTypes == null) { - mMigratedTypes = new ArraySet<>( - Preconditions.checkNotNull(mBundle.getStringArrayList(MIGRATED_TYPES_FIELD))); + if (mMigratedTypesCached == null) { + mMigratedTypesCached = new ArraySet<>(Preconditions.checkNotNull(mMigratedTypes)); } - return Collections.unmodifiableSet(mMigratedTypes); + return Collections.unmodifiableSet(mMigratedTypesCached); } /** @@ -151,27 +169,11 @@ */ @NonNull public Set getIncompatibleTypes() { - if (mIncompatibleTypes == null) { - mIncompatibleTypes = new ArraySet<>( - Preconditions.checkNotNull( - mBundle.getStringArrayList(INCOMPATIBLE_TYPES_FIELD))); + if (mIncompatibleTypesCached == null) { + mIncompatibleTypesCached = + new ArraySet<>(Preconditions.checkNotNull(mIncompatibleTypes)); } - return Collections.unmodifiableSet(mIncompatibleTypes); - } - - /** - * Translates the {@link SetSchemaResponse}'s bundle to {@link Builder}. - * @exportToFramework:hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @NonNull - // TODO(b/179302942) change to Builder(mBundle) powered by mBundle.deepCopy - public Builder toBuilder() { - return new Builder() - .addDeletedTypes(getDeletedTypes()) - .addIncompatibleTypes(getIncompatibleTypes()) - .addMigratedTypes(getMigratedTypes()) - .addMigrationFailures(mMigrationFailures); + return Collections.unmodifiableSet(mIncompatibleTypesCached); } /** Builder for {@link SetSchemaResponse} objects. */ @@ -182,6 +184,23 @@ private ArrayList mIncompatibleTypes = new ArrayList<>(); private boolean mBuilt = false; + /** + * Creates a new {@link SetSchemaResponse.Builder} from the given SetSchemaResponse. + * + * @exportToFramework:hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public Builder(@NonNull SetSchemaResponse setSchemaResponse) { + Preconditions.checkNotNull(setSchemaResponse); + mDeletedTypes.addAll(setSchemaResponse.getDeletedTypes()); + mIncompatibleTypes.addAll(setSchemaResponse.getIncompatibleTypes()); + mMigratedTypes.addAll(setSchemaResponse.getMigratedTypes()); + mMigrationFailures.addAll(setSchemaResponse.getMigrationFailures()); + } + + /** Create a {@link Builder} object} */ + public Builder() {} + /** Adds {@link MigrationFailure}s to the list of migration failures. */ @CanIgnoreReturnValue @NonNull @@ -266,15 +285,15 @@ /** Builds a {@link SetSchemaResponse} object. */ @NonNull public SetSchemaResponse build() { - Bundle bundle = new Bundle(); - bundle.putStringArrayList(INCOMPATIBLE_TYPES_FIELD, mIncompatibleTypes); - bundle.putStringArrayList(DELETED_TYPES_FIELD, mDeletedTypes); - bundle.putStringArrayList(MIGRATED_TYPES_FIELD, mMigratedTypes); mBuilt = true; // Avoid converting the potential thousands of MigrationFailures to Pracelable and // back just for put in bundle. In platform, we should set MigrationFailures in // AppSearchSession after we pass SetSchemaResponse via binder. - return new SetSchemaResponse(bundle, mMigrationFailures); + return new SetSchemaResponse( + mDeletedTypes, + mIncompatibleTypes, + mMigratedTypes, + mMigrationFailures); } private void resetIfBuilt() { @@ -288,18 +307,50 @@ } } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + SetSchemaResponseCreator.writeToParcel(this, dest, flags); + } + /** * The class represents a post-migrated {@link GenericDocument} that failed to be saved by * {@link AppSearchSession#setSchemaAsync}. */ - public static class MigrationFailure { - private static final String SCHEMA_TYPE_FIELD = "schemaType"; - private static final String NAMESPACE_FIELD = "namespace"; - private static final String DOCUMENT_ID_FIELD = "id"; - private static final String ERROR_MESSAGE_FIELD = "errorMessage"; - private static final String RESULT_CODE_FIELD = "resultCode"; + @SafeParcelable.Class(creator = "MigrationFailureCreator") + @SuppressWarnings("HiddenSuperclass") + public static class MigrationFailure extends AbstractSafeParcelable { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @NonNull + public static final Parcelable.Creator CREATOR = + new MigrationFailureCreator(); - private final Bundle mBundle; + @Field(id = 1, getter = "getNamespace") + private final String mNamespace; + @Field(id = 2, getter = "getDocumentId") + private final String mDocumentId; + @Field(id = 3, getter = "getSchemaType") + private final String mSchemaType; + @Field(id = 4) + @Nullable final String mErrorMessage; + @Field(id = 5) + final int mResultCode; + + @Constructor + MigrationFailure( + @Param(id = 1) @NonNull String namespace, + @Param(id = 2) @NonNull String documentId, + @Param(id = 3) @NonNull String schemaType, + @Param(id = 4) @Nullable String errorMessage, + @Param(id = 5) int resultCode) { + mNamespace = namespace; + mDocumentId = documentId; + mSchemaType = schemaType; + mErrorMessage = errorMessage; + mResultCode = resultCode; + } /** * Constructs a new {@link MigrationFailure}. @@ -315,49 +366,33 @@ @NonNull String documentId, @NonNull String schemaType, @NonNull AppSearchResult failedResult) { - mBundle = new Bundle(); - mBundle.putString(NAMESPACE_FIELD, Preconditions.checkNotNull(namespace)); - mBundle.putString(DOCUMENT_ID_FIELD, Preconditions.checkNotNull(documentId)); - mBundle.putString(SCHEMA_TYPE_FIELD, Preconditions.checkNotNull(schemaType)); + mNamespace = namespace; + mDocumentId = documentId; + mSchemaType = schemaType; Preconditions.checkNotNull(failedResult); Preconditions.checkArgument( !failedResult.isSuccess(), "failedResult was actually successful"); - mBundle.putString(ERROR_MESSAGE_FIELD, failedResult.getErrorMessage()); - mBundle.putInt(RESULT_CODE_FIELD, failedResult.getResultCode()); - } - - MigrationFailure(@NonNull Bundle bundle) { - mBundle = Preconditions.checkNotNull(bundle); - } - - /** - * Returns the Bundle of the {@link MigrationFailure}. - * - * @exportToFramework:hide - */ - @NonNull - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - public Bundle getBundle() { - return mBundle; + mErrorMessage = failedResult.getErrorMessage(); + mResultCode = failedResult.getResultCode(); } /** Returns the namespace of the {@link GenericDocument} that failed to be migrated. */ @NonNull public String getNamespace() { - return mBundle.getString(NAMESPACE_FIELD, /*defaultValue=*/""); + return mNamespace; } /** Returns the id of the {@link GenericDocument} that failed to be migrated. */ @NonNull public String getDocumentId() { - return mBundle.getString(DOCUMENT_ID_FIELD, /*defaultValue=*/""); + return mDocumentId; } /** Returns the schema type of the {@link GenericDocument} that failed to be migrated. */ @NonNull public String getSchemaType() { - return mBundle.getString(SCHEMA_TYPE_FIELD, /*defaultValue=*/""); + return mSchemaType; } /** @@ -366,8 +401,7 @@ */ @NonNull public AppSearchResult getAppSearchResult() { - return AppSearchResult.newFailedResult(mBundle.getInt(RESULT_CODE_FIELD), - mBundle.getString(ERROR_MESSAGE_FIELD, /*defaultValue=*/"")); + return AppSearchResult.newFailedResult(mResultCode, mErrorMessage); } @NonNull @@ -377,5 +411,12 @@ + getNamespace() + ", documentId: " + getDocumentId() + ", appSearchResult: " + getAppSearchResult().toString() + "}"; } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + MigrationFailureCreator.writeToParcel(this, dest, flags); + } } }
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
    index d95eff6..fd22846 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/StorageInfo.java
    
    @@ -16,39 +16,49 @@
     
     package androidx.appsearch.app;
     
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.RestrictTo;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
    -import androidx.core.util.Preconditions;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.StorageInfoCreator;
     
     /** The response class of {@code AppSearchSession#getStorageInfo}. */
    -public class StorageInfo {
    -
    -    private static final String SIZE_BYTES_FIELD = "sizeBytes";
    -    private static final String ALIVE_DOCUMENTS_COUNT = "aliveDocumentsCount";
    -    private static final String ALIVE_NAMESPACES_COUNT = "aliveNamespacesCount";
    -
    -    private final Bundle mBundle;
    -
    -    StorageInfo(@NonNull Bundle bundle) {
    -        mBundle = Preconditions.checkNotNull(bundle);
    -    }
    -
    -    /**
    -     * Returns the {@link Bundle} populated by this builder.
    -     * @exportToFramework:hide
    -     */
    -    @NonNull
    [email protected](creator = "StorageInfoCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class StorageInfo extends AbstractSafeParcelable {
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public Bundle getBundle() {
    -        return mBundle;
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR = new StorageInfoCreator();
    +
    +    @Field(id = 1, getter = "getSizeBytes")
    +    private long mSizeBytes;
    +
    +    @Field(id = 2, getter = "getAliveDocumentsCount")
    +    private int mAliveDocumentsCount;
    +
    +    @Field(id = 3, getter = "getAliveNamespacesCount")
    +    private int mAliveNamespacesCount;
    +
    +    @Constructor
    +    StorageInfo(
    +            @Param(id = 1) long sizeBytes,
    +            @Param(id = 2) int aliveDocumentsCount,
    +            @Param(id = 3) int aliveNamespacesCount) {
    +        mSizeBytes = sizeBytes;
    +        mAliveDocumentsCount = aliveDocumentsCount;
    +        mAliveNamespacesCount = aliveNamespacesCount;
         }
     
         /** Returns the estimated size of the session's database in bytes. */
         public long getSizeBytes() {
    -        return mBundle.getLong(SIZE_BYTES_FIELD);
    +        return mSizeBytes;
         }
     
         /**
    @@ -58,7 +68,7 @@
          * set in {@link GenericDocument.Builder#setTtlMillis}.
          */
         public int getAliveDocumentsCount() {
    -        return mBundle.getInt(ALIVE_DOCUMENTS_COUNT);
    +        return mAliveDocumentsCount;
         }
     
         /**
    @@ -69,7 +79,7 @@
          * set in {@link GenericDocument.Builder#setTtlMillis}.
          */
         public int getAliveNamespacesCount() {
    -        return mBundle.getInt(ALIVE_NAMESPACES_COUNT);
    +        return mAliveNamespacesCount;
         }
     
         /** Builder for {@link StorageInfo} objects. */
    @@ -105,11 +115,14 @@
             /** Builds a {@link StorageInfo} object. */
             @NonNull
             public StorageInfo build() {
    -            Bundle bundle = new Bundle();
    -            bundle.putLong(SIZE_BYTES_FIELD, mSizeBytes);
    -            bundle.putInt(ALIVE_DOCUMENTS_COUNT, mAliveDocumentsCount);
    -            bundle.putInt(ALIVE_NAMESPACES_COUNT, mAliveNamespacesCount);
    -            return new StorageInfo(bundle);
    +            return new StorageInfo(mSizeBytes, mAliveDocumentsCount, mAliveNamespacesCount);
             }
         }
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        StorageInfoCreator.writeToParcel(this, dest, flags);
    +    }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
    deleted file mode 100644
    index 2bea91f..0000000
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityDocument.java
    +++ /dev/null
    
    @@ -1,446 +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 androidx.appsearch.app;
    -
    -import android.os.Parcel;
    -
    -import androidx.annotation.NonNull;
    -import androidx.annotation.Nullable;
    -import androidx.annotation.RestrictTo;
    -import androidx.appsearch.annotation.CanIgnoreReturnValue;
    -import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    -import androidx.appsearch.safeparcel.SafeParcelable;
    -import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityDocumentCreator;
    -import androidx.collection.ArraySet;
    -
    -import java.util.ArrayList;
    -import java.util.Arrays;
    -import java.util.Collections;
    -import java.util.List;
    -import java.util.Map;
    -import java.util.Objects;
    -import java.util.Set;
    -
    -/**
    - * Holds the visibility settings that apply to a schema type.
    - * @exportToFramework:hide
    - */
    -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    [email protected](creator = "VisibilityDocumentCreator")
    -public final class VisibilityDocument extends AbstractSafeParcelable {
    -    @NonNull
    -    public static final VisibilityDocumentCreator CREATOR = new VisibilityDocumentCreator();
    -
    -    /**
    -     * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
    -     */
    -    public static final String SCHEMA_TYPE = "VisibilityType";
    -    /** Namespace of documents that contain visibility settings */
    -    public static final String NAMESPACE = "";
    -
    -    /**
    -     * Property that holds the list of platform-hidden schemas, as part of the visibility settings.
    -     */
    -    private static final String NOT_DISPLAYED_BY_SYSTEM_PROPERTY = "notPlatformSurfaceable";
    -
    -    /** Property that holds the package name that can access a schema. */
    -    private static final String PACKAGE_NAME_PROPERTY = "packageName";
    -
    -    /** Property that holds the SHA 256 certificate of the app that can access a schema. */
    -    private static final String SHA_256_CERT_PROPERTY = "sha256Cert";
    -
    -    /** Property that holds the required permissions to access the schema. */
    -    private static final String PERMISSION_PROPERTY = "permission";
    -
    -    // The initial schema version, one VisibilityDocument contains all visibility information for
    -    // whole package.
    -    public static final int SCHEMA_VERSION_DOC_PER_PACKAGE = 0;
    -
    -    // One VisibilityDocument contains visibility information for a single schema.
    -    public static final int SCHEMA_VERSION_DOC_PER_SCHEMA = 1;
    -
    -    // One VisibilityDocument contains visibility information for a single schema.
    -    public static final int SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA = 2;
    -
    -    public static final int SCHEMA_VERSION_LATEST = SCHEMA_VERSION_NESTED_PERMISSION_SCHEMA;
    -
    -    /**
    -     * Schema for the VisibilityStore's documents.
    -     *
    -     * 

    NOTE: If you update this, also update {@link #SCHEMA_VERSION_LATEST}. - */ - public static final AppSearchSchema - SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE) - .addProperty(new AppSearchSchema.BooleanPropertyConfig.Builder( - NOT_DISPLAYED_BY_SYSTEM_PROPERTY) - .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) - .build()) - .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(PACKAGE_NAME_PROPERTY) - .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) - .build()) - .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(SHA_256_CERT_PROPERTY) - .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) - .build()) - .addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(PERMISSION_PROPERTY, - VisibilityPermissionDocument.SCHEMA_TYPE) - .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) - .build()) - .build(); - - @NonNull - @Field(id = 1, getter = "getId") - private String mId; - - @Field(id = 2, getter = "isNotDisplayedBySystem") - private final boolean mIsNotDisplayedBySystem; - - @NonNull - @Field(id = 3, getter = "getPackageNames") - private final String[] mPackageNames; - - @NonNull - @Field(id = 4, getter = "getSha256Certs") - private final byte[][] mSha256Certs; - - @Nullable - @Field(id = 5, getter = "getPermissionDocuments") - private final VisibilityPermissionDocument[] mPermissionDocuments; - - @Nullable - // We still need to convert this class to a GenericDocument until we completely treat it - // differently in AppSearchImpl. - // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store - // visibility information. - private GenericDocument mGenericDocument; - - @Nullable - private Integer mHashCode; - - @Constructor - VisibilityDocument( - @Param(id = 1) @NonNull String id, - @Param(id = 2) boolean isNotDisplayedBySystem, - @Param(id = 3) @NonNull String[] packageNames, - @Param(id = 4) @NonNull byte[][] sha256Certs, - @Param(id = 5) @Nullable VisibilityPermissionDocument[] permissionDocuments) { - mId = Objects.requireNonNull(id); - mIsNotDisplayedBySystem = isNotDisplayedBySystem; - mPackageNames = Objects.requireNonNull(packageNames); - mSha256Certs = Objects.requireNonNull(sha256Certs); - mPermissionDocuments = permissionDocuments; - } - - /** - * Gets the id for this VisibilityDocument. - * - *

    This is being used as the document id when we convert a {@link VisibilityDocument} - * to a {@link GenericDocument}. - */ - @NonNull - public String getId() { - return mId; - } - - /** Returns whether this schema is visible to the system. */ - public boolean isNotDisplayedBySystem() { - return mIsNotDisplayedBySystem; - } - - /** - * Returns a package name array which could access this schema. Use {@link #getSha256Certs()} to - * get package's sha 256 certs. The same index of package names array and sha256Certs array - * represents same package. - */ - @NonNull - public String[] getPackageNames() { - return mPackageNames; - } - - /** - * Returns a package sha256Certs array which could access this schema. Use {@link - * #getPackageNames()} to get package's name. The same index of package names array and - * sha256Certs array represents same package. - */ - @NonNull - public byte[][] getSha256Certs() { - return mSha256Certs; - } - - /** Gets a list of {@link VisibilityDocument}. - * - *

    A {@link VisibilityDocument} holds all required permissions for the caller need to have - * to access the schema this {@link VisibilityDocument} presents. - */ - @Nullable - VisibilityPermissionDocument[] getPermissionDocuments() { - return mPermissionDocuments; - } - - /** - * Returns an array of Android Permissions that caller mush hold to access the schema this - * {@link VisibilityDocument} represents. - */ - @NonNull - public Set> getVisibleToPermissions() { - if (mPermissionDocuments == null) { - return Collections.emptySet(); - } - Set> visibleToPermissions = new ArraySet<>(mPermissionDocuments.length); - for (VisibilityPermissionDocument permissionDocument : mPermissionDocuments) { - Set requiredPermissions = permissionDocument.getAllRequiredPermissions(); - if (requiredPermissions != null) { - visibleToPermissions.add(requiredPermissions); - } - } - return visibleToPermissions; - } - - /** Build the List of {@link VisibilityDocument} from visibility settings. */ - @NonNull - public static List toVisibilityDocuments( - @NonNull SetSchemaRequest setSchemaRequest) { - Set searchSchemas = setSchemaRequest.getSchemas(); - Set schemasNotDisplayedBySystem = setSchemaRequest.getSchemasNotDisplayedBySystem(); - Map> schemasVisibleToPackages = - setSchemaRequest.getSchemasVisibleToPackages(); - Map>> schemasVisibleToPermissions = - setSchemaRequest.getRequiredPermissionsForSchemaTypeVisibility(); - List visibilityDocuments = new ArrayList<>(searchSchemas.size()); - for (AppSearchSchema searchSchema : searchSchemas) { - String schemaType = searchSchema.getSchemaType(); - VisibilityDocument.Builder documentBuilder = - new VisibilityDocument.Builder(/*id=*/ searchSchema.getSchemaType()); - documentBuilder.setNotDisplayedBySystem( - schemasNotDisplayedBySystem.contains(schemaType)); - - if (schemasVisibleToPackages.containsKey(schemaType)) { - documentBuilder.addVisibleToPackages(schemasVisibleToPackages.get(schemaType)); - } - - if (schemasVisibleToPermissions.containsKey(schemaType)) { - documentBuilder.setVisibleToPermissions( - schemasVisibleToPermissions.get(schemaType)); - } - visibilityDocuments.add(documentBuilder.build()); - } - return visibilityDocuments; - } - - /** - * Generates a {@link GenericDocument} from the current class. - * - *

    This conversion is needed until we don't treat Visibility related documents as - * {@link GenericDocument}s internally. - */ - @NonNull - public GenericDocument toGenericDocument() { - if (mGenericDocument == null) { - GenericDocument.Builder builder = new GenericDocument.Builder<>( - NAMESPACE, mId, SCHEMA_TYPE); - builder.setPropertyBoolean(NOT_DISPLAYED_BY_SYSTEM_PROPERTY, mIsNotDisplayedBySystem); - builder.setPropertyString(PACKAGE_NAME_PROPERTY, mPackageNames); - builder.setPropertyBytes(SHA_256_CERT_PROPERTY, mSha256Certs); - - // Generate an array of GenericDocument for VisibilityPermissionDocument. - if (mPermissionDocuments != null) { - GenericDocument[] permissionGenericDocs = - new GenericDocument[mPermissionDocuments.length]; - for (int i = 0; i < mPermissionDocuments.length; ++i) { - permissionGenericDocs[i] = mPermissionDocuments[i].toGenericDocument(); - } - builder.setPropertyDocument(PERMISSION_PROPERTY, permissionGenericDocs); - } - - // The creationTimestamp doesn't matter for Visibility documents. - // But to make tests pass, we set it 0 so two GenericDocuments generated from - // the same VisibilityDocument can be same. - builder.setCreationTimestampMillis(0L); - - mGenericDocument = builder.build(); - } - return mGenericDocument; - } - - @Override - public int hashCode() { - if (mHashCode == null) { - mHashCode = Objects.hash( - mId, - mIsNotDisplayedBySystem, - Arrays.hashCode(mPackageNames), - Arrays.deepHashCode(mSha256Certs), - Arrays.hashCode(mPermissionDocuments)); - } - return mHashCode; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (!(other instanceof VisibilityDocument)) { - return false; - } - VisibilityDocument otherVisibilityDocument = (VisibilityDocument) other; - return mId.equals(otherVisibilityDocument.mId) - && mIsNotDisplayedBySystem == otherVisibilityDocument.mIsNotDisplayedBySystem - && Arrays.equals( - mPackageNames, otherVisibilityDocument.mPackageNames) - && Arrays.deepEquals( - mSha256Certs, otherVisibilityDocument.mSha256Certs) - && Arrays.equals( - mPermissionDocuments, otherVisibilityDocument.mPermissionDocuments); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - VisibilityDocumentCreator.writeToParcel(this, dest, flags); - } - - /** Builder for {@link VisibilityDocument}. */ - public static final class Builder { - private final Set mPackageIdentifiers = new ArraySet<>(); - private String mId; - private boolean mIsNotDisplayedBySystem; - private VisibilityPermissionDocument[] mPermissionDocuments; - - /** - * Creates a {@link Builder} for a {@link VisibilityDocument}. - * - * @param id The SchemaType of the {@link AppSearchSchema} that this {@link - * VisibilityDocument} represents. The package and database prefix will be added in - * server side. We are using prefixed schema type to be the final id of this {@link - * VisibilityDocument}. - */ - public Builder(@NonNull String id) { - mId = Objects.requireNonNull(id); - } - - /** - * Constructs a {@link VisibilityDocument} from a {@link GenericDocument}. - * - *

    This constructor is still needed until we don't treat Visibility related documents as - * {@link GenericDocument}s internally. - */ - public Builder(@NonNull GenericDocument genericDocument) { - Objects.requireNonNull(genericDocument); - - mId = genericDocument.getId(); - mIsNotDisplayedBySystem = genericDocument.getPropertyBoolean( - NOT_DISPLAYED_BY_SYSTEM_PROPERTY); - - String[] packageNames = genericDocument.getPropertyStringArray(PACKAGE_NAME_PROPERTY); - byte[][] sha256Certs = genericDocument.getPropertyBytesArray(SHA_256_CERT_PROPERTY); - for (int i = 0; i < packageNames.length; ++i) { - mPackageIdentifiers.add(new PackageIdentifier(packageNames[i], sha256Certs[i])); - } - - GenericDocument[] permissionDocs = - genericDocument.getPropertyDocumentArray(PERMISSION_PROPERTY); - if (permissionDocs != null) { - mPermissionDocuments = new VisibilityPermissionDocument[permissionDocs.length]; - for (int i = 0; i < permissionDocs.length; ++i) { - mPermissionDocuments[i] = new VisibilityPermissionDocument.Builder( - permissionDocs[i]).build(); - } - } - } - - public Builder(@NonNull VisibilityDocument visibilityDocument) { - Objects.requireNonNull(visibilityDocument); - - mIsNotDisplayedBySystem = visibilityDocument.mIsNotDisplayedBySystem; - mPermissionDocuments = visibilityDocument.mPermissionDocuments; - for (int i = 0; i < visibilityDocument.mPackageNames.length; ++i) { - mPackageIdentifiers.add(new PackageIdentifier(visibilityDocument.mPackageNames[i], - visibilityDocument.mSha256Certs[i])); - } - } - - /** Sets id. */ - @CanIgnoreReturnValue - @NonNull - public Builder setId(@NonNull String id) { - mId = Objects.requireNonNull(id); - return this; - } - - /** Sets whether this schema has opted out of platform surfacing. */ - @CanIgnoreReturnValue - @NonNull - public Builder setNotDisplayedBySystem(boolean notDisplayedBySystem) { - mIsNotDisplayedBySystem = notDisplayedBySystem; - return this; - } - - /** Add {@link PackageIdentifier} of packages which has access to this schema. */ - @CanIgnoreReturnValue - @NonNull - public Builder addVisibleToPackages(@NonNull Set packageIdentifiers) { - mPackageIdentifiers.addAll(Objects.requireNonNull(packageIdentifiers)); - return this; - } - - /** Add {@link PackageIdentifier} of packages which has access to this schema. */ - @CanIgnoreReturnValue - @NonNull - public Builder addVisibleToPackage(@NonNull PackageIdentifier packageIdentifier) { - mPackageIdentifiers.add(Objects.requireNonNull(packageIdentifier)); - return this; - } - - /** - * Sets required permission sets for a package needs to hold to the schema this {@link - * VisibilityDocument} represents. - * - *

    The querier could have access if they holds ALL required permissions of ANY of the - * individual value sets. - */ - @CanIgnoreReturnValue - @NonNull - public Builder setVisibleToPermissions(@NonNull Set> visibleToPermissions) { - Objects.requireNonNull(visibleToPermissions); - mPermissionDocuments = - new VisibilityPermissionDocument[visibleToPermissions.size()]; - int i = 0; - for (Set allRequiredPermissions : visibleToPermissions) { - mPermissionDocuments[i++] = - new VisibilityPermissionDocument.Builder( - NAMESPACE, /*id=*/ String.valueOf(i)) - .setVisibleToAllRequiredPermissions(allRequiredPermissions) - .build(); - } - return this; - } - - /** Build a {@link VisibilityDocument} */ - @NonNull - public VisibilityDocument build() { - String[] packageNames = new String[mPackageIdentifiers.size()]; - byte[][] sha256Certs = new byte[mPackageIdentifiers.size()][32]; - int i = 0; - for (PackageIdentifier packageIdentifier : mPackageIdentifiers) { - packageNames[i] = packageIdentifier.getPackageName(); - sha256Certs[i] = packageIdentifier.getSha256Certificate(); - ++i; - } - return new VisibilityDocument(mId, mIsNotDisplayedBySystem, - packageNames, sha256Certs, mPermissionDocuments); - } - } -} -

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java
    new file mode 100644
    index 0000000..40e0f81
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionConfig.java
    
    @@ -0,0 +1,185 @@
    +/*
    + * 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 androidx.appsearch.app;
    +
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionConfigCreator;
    +import androidx.collection.ArraySet;
    +
    +import java.util.Arrays;
    +import java.util.Objects;
    +import java.util.Set;
    +
    +/**
    + * The config class that holds all required permissions for a caller need to hold to access the
    + * schema which the outer {@link SchemaVisibilityConfig} represents.
    + * @exportToFramework:hide
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    [email protected](creator = "VisibilityPermissionConfigCreator")
    +public final class VisibilityPermissionConfig extends AbstractSafeParcelable {
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR =
    +            new VisibilityPermissionConfigCreator();
    +
    +    /**
    +     * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
    +     */
    +    public static final String SCHEMA_TYPE = "VisibilityPermissionType";
    +
    +    /** Property that holds the required permissions to access the schema. */
    +    public static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
    +
    +    /**
    +     * Schema for the VisibilityStore's documents.
    +     *
    +     * 

    NOTE: If you update this, also update schema version number in + * VisibilityToDocumentConverter + */ + public static final AppSearchSchema + SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE) + .addProperty(new AppSearchSchema.LongPropertyConfig + .Builder(ALL_REQUIRED_PERMISSIONS_PROPERTY) + .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) + .build()) + .build(); + + @Nullable + @Field(id = 1) + final int[] mAllRequiredPermissions; + + @Nullable + // We still need to convert this class to a GenericDocument until we completely treat it + // differently in AppSearchImpl. + // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store + // visibility information. + private GenericDocument mGenericDocument; + + @Nullable + private Integer mHashCode; + + @Constructor + VisibilityPermissionConfig(@Param(id = 1) @Nullable int[] allRequiredPermissions) { + mAllRequiredPermissions = allRequiredPermissions; + } + + /** + * Sets a set of Android Permissions that caller must hold to access the schema that the + * outer {@link SchemaVisibilityConfig} represents. + */ + public VisibilityPermissionConfig(@NonNull Set allRequiredPermissions) { + mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions)); + } + + /** + * Returns an array of Android Permissions that caller mush hold to access the schema that the + * outer {@link SchemaVisibilityConfig} represents. + */ + @Nullable + public Set getAllRequiredPermissions() { + return toIntegerSet(mAllRequiredPermissions); + } + + @NonNull + private static int[] toInts(@NonNull Set properties) { + int[] outputs = new int[properties.size()]; + int i = 0; + for (int property : properties) { + outputs[i++] = property; + } + return outputs; + } + + @Nullable + private static Set toIntegerSet(@Nullable int[] properties) { + if (properties == null) { + return null; + } + Set outputs = new ArraySet<>(properties.length); + for (int property : properties) { + outputs.add(property); + } + return outputs; + } + + /** + * Generates a {@link GenericDocument} from the current class. + * + *

    This conversion is needed until we don't treat Visibility related documents as + * {@link GenericDocument}s internally. + */ + @NonNull + public GenericDocument toGenericDocument() { + 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); + + if (mAllRequiredPermissions != null) { + // GenericDocument only supports long, so int[] needs to be converted to + // long[] here. + long[] longs = new long[mAllRequiredPermissions.length]; + for (int i = 0; i < mAllRequiredPermissions.length; ++i) { + longs[i] = mAllRequiredPermissions[i]; + } + builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs); + } + + // The creationTimestamp doesn't matter for Visibility documents. + // But to make tests pass, we set it 0 so two GenericDocuments generated from + // the same VisibilityPermissionConfig can be same. + builder.setCreationTimestampMillis(0L); + + mGenericDocument = builder.build(); + } + return mGenericDocument; + } + + @Override + public int hashCode() { + if (mHashCode == null) { + mHashCode = Arrays.hashCode(mAllRequiredPermissions); + } + return mHashCode; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof VisibilityPermissionConfig)) { + return false; + } + VisibilityPermissionConfig otherVisibilityPermissionConfig = + (VisibilityPermissionConfig) other; + return Arrays.equals(mAllRequiredPermissions, + otherVisibilityPermissionConfig.mAllRequiredPermissions); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + VisibilityPermissionConfigCreator.writeToParcel(this, dest, flags); + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
    deleted file mode 100644
    index 54269fd..0000000
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/VisibilityPermissionDocument.java
    +++ /dev/null
    
    @@ -1,269 +0,0 @@
    -/*
    - * Copyright 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 androidx.appsearch.app;
    -
    -import android.os.Parcel;
    -
    -import androidx.annotation.NonNull;
    -import androidx.annotation.Nullable;
    -import androidx.annotation.RestrictTo;
    -import androidx.appsearch.annotation.CanIgnoreReturnValue;
    -import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    -import androidx.appsearch.safeparcel.SafeParcelable;
    -import androidx.appsearch.safeparcel.stub.StubCreators.VisibilityPermissionDocumentCreator;
    -import androidx.collection.ArraySet;
    -
    -import java.util.Arrays;
    -import java.util.Objects;
    -import java.util.Set;
    -
    -/**
    - * The nested document that holds all required permissions for a caller need to hold to access the
    - * schema which the outer {@link VisibilityDocument} represents.
    - * @exportToFramework:hide
    - */
    -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    [email protected](creator = "VisibilityPermissionDocumentCreator")
    -public final class VisibilityPermissionDocument extends AbstractSafeParcelable {
    -    @NonNull
    -    public static final VisibilityPermissionDocumentCreator CREATOR =
    -            new VisibilityPermissionDocumentCreator();
    -
    -    /**
    -     * The Schema type for documents that hold AppSearch's metadata, such as visibility settings.
    -     */
    -    public static final String SCHEMA_TYPE = "VisibilityPermissionType";
    -
    -    /** Property that holds the required permissions to access the schema. */
    -    private static final String ALL_REQUIRED_PERMISSIONS_PROPERTY = "allRequiredPermissions";
    -
    -    /**
    -     * Schema for the VisibilityStore's documents.
    -     *
    -     * 

    NOTE: If you update this, also update {@link VisibilityDocument#SCHEMA_VERSION_LATEST}. - */ - public static final AppSearchSchema - SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE) - .addProperty(new AppSearchSchema.LongPropertyConfig - .Builder(ALL_REQUIRED_PERMISSIONS_PROPERTY) - .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED) - .build()) - .build(); - - @NonNull - @Field(id = 1, getter = "getId") - private final String mId; - - @NonNull - @Field(id = 2, getter = "getNamespace") - private final String mNamespace; - - @Nullable - @Field(id = 3, getter = "getAllRequiredPermissionsInts") - // SafeParcelable doesn't support Set, so we have to convert it to int[]. - private final int[] mAllRequiredPermissions; - - @Nullable - // We still need to convert this class to a GenericDocument until we completely treat it - // differently in AppSearchImpl. - // TODO(b/298118943) Remove this once internally we don't use GenericDocument to store - // visibility information. - private GenericDocument mGenericDocument; - - @Nullable - private Integer mHashCode; - - @Constructor - VisibilityPermissionDocument( - @Param(id = 1) @NonNull String id, - @Param(id = 2) @NonNull String namespace, - @Param(id = 3) @Nullable int[] allRequiredPermissions) { - mId = Objects.requireNonNull(id); - mNamespace = Objects.requireNonNull(namespace); - mAllRequiredPermissions = allRequiredPermissions; - } - - /** - * Gets the id for this {@link VisibilityPermissionDocument}. - * - *

    This is being used as the document id when we convert a - * {@link VisibilityPermissionDocument} to a {@link GenericDocument}. - */ - @NonNull - public String getId() { - return mId; - } - - /** - * Gets the namespace for this {@link VisibilityPermissionDocument}. - * - *

    This is being used as the namespace when we convert a - * {@link VisibilityPermissionDocument} to a {@link GenericDocument}. - */ - @NonNull - public String getNamespace() { - return mNamespace; - } - - /** Gets the required Android Permissions in an int array. */ - @Nullable - int[] getAllRequiredPermissionsInts() { - return mAllRequiredPermissions; - } - - /** - * Returns an array of Android Permissions that caller mush hold to access the schema that the - * outer {@link VisibilityDocument} represents. - */ - @Nullable - public Set getAllRequiredPermissions() { - return toIntegerSet(mAllRequiredPermissions); - } - - @NonNull - private static int[] toInts(@NonNull Set properties) { - int[] outputs = new int[properties.size()]; - int i = 0; - for (int property : properties) { - outputs[i++] = property; - } - return outputs; - } - - @Nullable - private static Set toIntegerSet(@Nullable int[] properties) { - if (properties == null) { - return null; - } - Set outputs = new ArraySet<>(properties.length); - for (int property : properties) { - outputs.add(property); - } - return outputs; - } - - /** - * Generates a {@link GenericDocument} from the current class. - * - *

    This conversion is needed until we don't treat Visibility related documents as - * {@link GenericDocument}s internally. - */ - @NonNull - public GenericDocument toGenericDocument() { - if (mGenericDocument == null) { - GenericDocument.Builder builder = new GenericDocument.Builder<>( - mNamespace, mId, SCHEMA_TYPE); - - if (mAllRequiredPermissions != null) { - // GenericDocument only supports long, so int[] needs to be converted to - // long[] here. - long[] longs = new long[mAllRequiredPermissions.length]; - for (int i = 0; i < mAllRequiredPermissions.length; ++i) { - longs[i] = mAllRequiredPermissions[i]; - } - builder.setPropertyLong(ALL_REQUIRED_PERMISSIONS_PROPERTY, longs); - } - - mGenericDocument = builder.build(); - } - return mGenericDocument; - } - - @Override - public int hashCode() { - if (mHashCode == null) { - mHashCode = Objects.hash(mId, mNamespace, Arrays.hashCode(mAllRequiredPermissions)); - } - return mHashCode; - } - - @Override - public boolean equals(@Nullable Object other) { - if (this == other) { - return true; - } - if (!(other instanceof VisibilityPermissionDocument)) { - return false; - } - VisibilityPermissionDocument otherVisibilityPermissionDocument = - (VisibilityPermissionDocument) other; - return mId.equals(otherVisibilityPermissionDocument.mId) - && mNamespace.equals(otherVisibilityPermissionDocument.mNamespace) - && Arrays.equals( - mAllRequiredPermissions, - otherVisibilityPermissionDocument.mAllRequiredPermissions); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - VisibilityPermissionDocumentCreator.writeToParcel(this, dest, flags); - } - - /** Builder for {@link VisibilityPermissionDocument}. */ - public static final class Builder { - private String mId; - private String mNamespace; - private int[] mAllRequiredPermissions; - - /** - * Constructs a {@link VisibilityPermissionDocument} from a {@link GenericDocument}. - * - *

    This constructor is still needed until we don't treat Visibility related documents as - * {@link GenericDocument}s internally. - */ - public Builder(@NonNull GenericDocument genericDocument) { - Objects.requireNonNull(genericDocument); - mId = genericDocument.getId(); - mNamespace = genericDocument.getNamespace(); - // GenericDocument only supports long[], so we need to convert it back to int[]. - long[] longs = genericDocument.getPropertyLongArray( - ALL_REQUIRED_PERMISSIONS_PROPERTY); - if (longs != null) { - mAllRequiredPermissions = new int[longs.length]; - for (int i = 0; i < longs.length; ++i) { - mAllRequiredPermissions[i] = (int) longs[i]; - } - } - } - - /** Creates a {@link VisibilityDocument.Builder} for a {@link VisibilityDocument}. */ - public Builder(@NonNull String namespace, @NonNull String id) { - mNamespace = Objects.requireNonNull(namespace); - mId = Objects.requireNonNull(id); - } - - /** - * Sets a set of Android Permissions that caller mush hold to access the schema that the - * outer {@link VisibilityDocument} represents. - */ - @CanIgnoreReturnValue - @NonNull - public Builder setVisibleToAllRequiredPermissions( - @NonNull Set allRequiredPermissions) { - mAllRequiredPermissions = toInts(Objects.requireNonNull(allRequiredPermissions)); - return this; - } - - /** Builds a {@link VisibilityPermissionDocument} */ - @NonNull - public VisibilityPermissionDocument build() { - return new VisibilityPermissionDocument(mId, - mNamespace, - mAllRequiredPermissions); - } - } -}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java
    new file mode 100644
    index 0000000..4fb00e1
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnderInitialization.java
    
    @@ -0,0 +1,54 @@
    +/*
    + * 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 androidx.appsearch.checker.initialization.qual;
    +
    +import androidx.annotation.RestrictTo;
    +
    +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.
    +
    +/**
    + * 
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +@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/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java
    new file mode 100644
    index 0000000..8faa0dd
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/initialization/qual/UnknownInitialization.java
    
    @@ -0,0 +1,54 @@
    +/*
    + * 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 androidx.appsearch.checker.initialization.qual;
    +
    +import androidx.annotation.RestrictTo;
    +
    +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.
    +
    +/**
    + * 
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +@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/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java
    new file mode 100644
    index 0000000..c9137c5
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/Nullable.java
    
    @@ -0,0 +1,55 @@
    +/*
    + * 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 androidx.appsearch.checker.nullness.qual;
    +
    +
    +// This is an annotation stub to avoid dependencies on annotations that aren't
    +// in the Android platform source tree.
    +
    +import androidx.annotation.RestrictTo;
    +
    +import java.lang.annotation.ElementType;
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +import java.lang.annotation.Target;
    +
    +/**
    + * 
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +@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/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java
    new file mode 100644
    index 0000000..4c39b9b
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/checker/nullness/qual/RequiresNonNull.java
    
    @@ -0,0 +1,54 @@
    +/*
    + * 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 androidx.appsearch.checker.nullness.qual;
    +
    +import androidx.annotation.RestrictTo;
    +
    +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.
    +
    +/**
    + * 
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +@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/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
    index 2930d2d..b39435f 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/exceptions/AppSearchException.java
    
    @@ -27,7 +27,7 @@
      * for propagating to the 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.
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
    new file mode 100644
    index 0000000..693be73
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/FlaggedApi.java
    
    @@ -0,0 +1,31 @@
    +/*
    + * 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 androidx.appsearch.flags;
    +
    +import androidx.annotation.RestrictTo;
    +
    +/**
    + * Indicates an API is part of a feature that is guarded by an aconfig flag in the framework, and
    + * only available if the flag is enabled.
    + *
    + * 

    Our own Jetpack version is created here for code sync purpose. + */ +// @exportToFramework:skipFile() +@RestrictTo(RestrictTo.Scope.LIBRARY) +public @interface FlaggedApi { + String value(); +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
    new file mode 100644
    index 0000000..5ad11f3
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/flags/Flags.java
    
    @@ -0,0 +1,242 @@
    +/*
    + * 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.
    + */
    +
    +// @exportToFramework:skipFile()
    +package androidx.appsearch.flags;
    +
    +
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.app.AppSearchSchema;
    +
    +import java.util.Collection;
    +
    +/**
    + * Flags to control different features.
    + *
    + * 

    In Jetpack, those values can't be changed during runtime. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +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 the "tokenize" function in list filter query expressions. */ + public static final String FLAG_ENABLE_LIST_FILTER_TOKENIZE_FUNCTION = + FLAG_PREFIX + "enable_list_filter_tokenize_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 androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and + * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties}. + */ + public static final String FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES = + FLAG_PREFIX + "enable_search_spec_filter_properties"; + /** + * Enable the {@link androidx.appsearch.app.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 androidx.appsearch.app.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 androidx.appsearch.app.EnterpriseGlobalSearchSession}. */ + public static final String FLAG_ENABLE_ENTERPRISE_GLOBAL_SEARCH_SESSION = + FLAG_PREFIX + "enable_enterprise_global_search_session"; + + /** + * Enables {@link android.app.appsearch.functions.AppFunctionManager} and app functions related + * stuff. + */ + public static final String FLAG_ENABLE_APP_FUNCTIONS = FLAG_PREFIX + "enable_app_functions"; + + /** + * Enable {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and + * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} which were previously + * hidden. + */ + public static final String FLAG_ENABLE_RESULT_DENIED_AND_RESULT_RATE_LIMITED = + FLAG_PREFIX + "enable_result_denied_and_result_rate_limited"; + + /** + * Enables {@link AppSearchSchema#getParentTypes()}, + * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of + * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}}. + */ + public static final String FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES = + FLAG_PREFIX + "enable_get_parent_types_and_indexable_nested_properties"; + + /** Enables embedding search related APIs. */ + public static final String FLAG_ENABLE_SCHEMA_EMBEDDING_PROPERTY_CONFIG = + FLAG_PREFIX + "enable_schema_embedding_property_config"; + + /** Enables informational ranking expressions. */ + public static final String FLAG_ENABLE_INFORMATIONAL_RANKING_EXPRESSIONS = + FLAG_PREFIX + "enable_informational_ranking_expressions"; + + // 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 androidx.appsearch.app.SearchSpec.Builder#addFilterProperties} and + * {@link androidx.appsearch.app.SearchSuggestionSpec.Builder#addFilterProperties} should be + * enabled. + */ + public static boolean enableSearchSpecFilterProperties() { + return true; + } + + /** + * Whether the {@link androidx.appsearch.app.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 androidx.appsearch.app.GenericDocument.Builder#setNamespace(String)}, + * {@link androidx.appsearch.app.GenericDocument.Builder#setId(String)}, + * {@link androidx.appsearch.app.GenericDocument.Builder#setSchemaType(String)}, and + * {@link androidx.appsearch.app.GenericDocument.Builder#clearProperty(String)} + * should be enabled. + */ + public static boolean enableGenericDocumentBuilderHiddenMethods() { + return true; + } + + /** + * Whether + * {@link androidx.appsearch.app.SetSchemaRequest.Builder #setSchemaTypeVisibilityForConfigs} + * should be enabled. + */ + public static boolean enableSetSchemaVisibleToConfigs() { + return true; + } + + /** Whether {@link androidx.appsearch.app.EnterpriseGlobalSearchSession} should be enabled. */ + public static boolean enableEnterpriseGlobalSearchSession() { + return true; + } + + /** + * Whether {@link androidx.appsearch.app.AppSearchResult#RESULT_DENIED} and + * {@link androidx.appsearch.app.AppSearchResult#RESULT_RATE_LIMITED} should be enabled. + */ + public static boolean enableResultDeniedAndResultRateLimited() { + return true; + } + + /** + * Whether {@link AppSearchSchema#getParentTypes()}, + * {@link AppSearchSchema.DocumentPropertyConfig#getIndexableNestedProperties()} and variants of + * {@link AppSearchSchema.DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection)}} + * should be enabled. + */ + public static boolean enableGetParentTypesAndIndexableNestedProperties() { + return true; + } + + /** Whether embedding search related APIs should be enabled. */ + public static boolean enableSchemaEmbeddingPropertyConfig() { + return true; + } + + /** Whether the "tokenize" function in list filter query expressions should be enabled. */ + public static boolean enableListFilterTokenizeFunction() { + return true; + } + + /** Whether informational ranking expressions should be enabled. */ + public static boolean enableInformationalRankingExpressions() { + return true; + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
    index 903929b..baae8d1 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/observer/ObserverSpec.java
    
    @@ -17,7 +17,8 @@
     package androidx.appsearch.observer;
     
     import android.annotation.SuppressLint;
    -import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    @@ -27,6 +28,11 @@
     import androidx.appsearch.app.DocumentClassFactory;
     import androidx.appsearch.app.DocumentClassFactoryRegistry;
     import androidx.appsearch.exceptions.AppSearchException;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.appsearch.safeparcel.AbstractSafeParcelable;
    +import androidx.appsearch.safeparcel.SafeParcelable;
    +import androidx.appsearch.safeparcel.stub.StubCreators.ObserverSpecCreator;
     import androidx.collection.ArraySet;
     import androidx.core.util.Preconditions;
     
    @@ -41,31 +47,27 @@
      * Configures the types, namespaces and other properties that {@link ObserverCallback} instances
      * match against.
      */
    -public final class ObserverSpec {
    -    private static final String FILTER_SCHEMA_FIELD = "filterSchema";
    [email protected](creator = "ObserverSpecCreator")
    +@SuppressWarnings("HiddenSuperclass")
    +public final class ObserverSpec extends AbstractSafeParcelable {
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR =
    +            new ObserverSpecCreator();
     
    -    private final Bundle mBundle;
    +    @Field(id = 1)
    +    final List mFilterSchemas;
     
         /** Populated on first use */
    -    @Nullable
    -    private volatile Set mFilterSchemas;
    +    @Nullable private volatile Set mFilterSchemasCached;
     
         /** @exportToFramework:hide */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    public ObserverSpec(@NonNull Bundle bundle) {
    -        Preconditions.checkNotNull(bundle);
    -        mBundle = bundle;
    -    }
    -
    -    /**
    -     * Returns the {@link Bundle} backing this spec.
    -     *
    -     * @exportToFramework:hide
    -     */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -    @NonNull
    -    public Bundle getBundle() {
    -        return mBundle;
    +    @Constructor
    +    public ObserverSpec(
    +            @Param(id = 1) @NonNull List filterSchemas) {
    +        mFilterSchemas = Preconditions.checkNotNull(filterSchemas);
         }
     
         /**
    @@ -75,15 +77,14 @@
          */
         @NonNull
         public Set getFilterSchemas() {
    -        if (mFilterSchemas == null) {
    -            List schemas = mBundle.getStringArrayList(FILTER_SCHEMA_FIELD);
    -            if (schemas == null) {
    -                mFilterSchemas = Collections.emptySet();
    +        if (mFilterSchemasCached == null) {
    +            if (mFilterSchemas == null) {
    +                mFilterSchemasCached = Collections.emptySet();
                 } else {
    -                mFilterSchemas = Collections.unmodifiableSet(new ArraySet<>(schemas));
    +                mFilterSchemasCached = Collections.unmodifiableSet(new ArraySet<>(mFilterSchemas));
                 }
             }
    -        return mFilterSchemas;
    +        return mFilterSchemasCached;
         }
     
         /** Builder for {@link ObserverSpec} instances. */
    @@ -134,7 +135,7 @@
             @SuppressLint("MissingGetterMatchingBuilder")
             @CanIgnoreReturnValue
             @NonNull
    -        public Builder addFilterDocumentClasses(@NonNull Class... documentClasses)
    +        public Builder addFilterDocumentClasses(@NonNull java.lang.Class... documentClasses)
                     throws AppSearchException {
                 Preconditions.checkNotNull(documentClasses);
                 resetIfBuilt();
    @@ -155,12 +156,13 @@
             @CanIgnoreReturnValue
             @NonNull
             public Builder addFilterDocumentClasses(
    -                @NonNull Collection> documentClasses) throws AppSearchException {
    +                @NonNull Collection> documentClasses)
    +                throws AppSearchException {
                 Preconditions.checkNotNull(documentClasses);
                 resetIfBuilt();
                 List schemas = new ArrayList<>(documentClasses.size());
                 DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
    -            for (Class documentClass : documentClasses) {
    +            for (java.lang.Class documentClass : documentClasses) {
                     DocumentClassFactory factory = registry.getOrCreateFactory(documentClass);
                     schemas.add(factory.getSchemaName());
                 }
    @@ -172,10 +174,8 @@
             /** Constructs a new {@link ObserverSpec} from the contents of this builder. */
             @NonNull
             public ObserverSpec build() {
    -            Bundle bundle = new Bundle();
    -            bundle.putStringArrayList(FILTER_SCHEMA_FIELD, mFilterSchemas);
                 mBuilt = true;
    -            return new ObserverSpec(bundle);
    +            return new ObserverSpec(mFilterSchemas);
             }
     
             private void resetIfBuilt() {
    @@ -185,4 +185,11 @@
                 }
             }
         }
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        ObserverSpecCreator.writeToParcel(this, dest, flags);
    +    }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
    index a677ff8..bdc4bb5 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/AbstractSafeParcelable.java
    
    @@ -211,4 +211,10 @@
          */
         public void writeToParcel(@NonNull Parcel dest, int flags) {
         }
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @Override
    +    public final int describeContents() {
    +        return 0;
    +    }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
    index d921290..983e280 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcel.java
    
    @@ -16,20 +16,22 @@
     
     package androidx.appsearch.safeparcel;
     
    +import android.annotation.SuppressLint;
     import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
    -import androidx.annotation.VisibleForTesting;
     import androidx.appsearch.annotation.CanIgnoreReturnValue;
     import androidx.appsearch.app.AppSearchSchema;
     import androidx.appsearch.app.AppSearchSession;
    +import androidx.appsearch.app.EmbeddingVector;
     import androidx.appsearch.app.GenericDocument;
    -import androidx.appsearch.safeparcel.stub.StubCreators.GenericDocumentParcelCreator;
     import androidx.collection.ArrayMap;
     
    -import java.util.Arrays;
    +import java.util.ArrayList;
    +import java.util.List;
     import java.util.Map;
     import java.util.Objects;
     import java.util.Set;
    @@ -41,9 +43,11 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
    -public final class GenericDocumentParcel extends AbstractSafeParcelable {
    +// 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 GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable {
         @NonNull
    -    public static final GenericDocumentParcelCreator CREATOR =
    +    public static final Parcelable.Creator CREATOR =
                 new GenericDocumentParcelCreator();
     
         /** The default score of document. */
    @@ -83,7 +87,15 @@
          */
         @Field(id = 7, getter = "getProperties")
         @NonNull
    -    private final PropertyParcel[] mProperties;
    +    private final List mProperties;
    +
    +    /**
    +     * Contains all parent properties for this {@link GenericDocument} in a list.
    +     *
    +     */
    +    @Field(id = 8, getter = "getParentTypes")
    +    @Nullable
    +    private final List mParentTypes;
     
         /**
          * Contains all properties in {@link GenericDocument} to support getting properties via name
    @@ -110,9 +122,10 @@
                 @Param(id = 4) long creationTimestampMillis,
                 @Param(id = 5) long ttlMillis,
                 @Param(id = 6) int score,
    -            @Param(id = 7) @NonNull PropertyParcel[] properties) {
    +            @Param(id = 7) @NonNull List properties,
    +            @Param(id = 8) @Nullable List parentTypes) {
             this(namespace, id, schemaType, creationTimestampMillis, ttlMillis, score,
    -                properties, createPropertyMapFromPropertyArray(properties));
    +                properties, createPropertyMapFromPropertyArray(properties), parentTypes);
         }
     
         /**
    @@ -128,8 +141,9 @@
                 long creationTimestampMillis,
                 long ttlMillis,
                 int score,
    -            @NonNull PropertyParcel[] properties,
    -            @NonNull Map propertyMap) {
    +            @NonNull List properties,
    +            @NonNull Map propertyMap,
    +            @Nullable List parentTypes) {
             mNamespace = Objects.requireNonNull(namespace);
             mId = Objects.requireNonNull(id);
             mSchemaType = Objects.requireNonNull(schemaType);
    @@ -138,14 +152,15 @@
             mScore = score;
             mProperties = Objects.requireNonNull(properties);
             mPropertyMap = Objects.requireNonNull(propertyMap);
    +        mParentTypes = parentTypes;
         }
     
         private static Map createPropertyMapFromPropertyArray(
    -            @NonNull PropertyParcel[] properties) {
    +            @NonNull List properties) {
             Objects.requireNonNull(properties);
    -        Map propertyMap = new ArrayMap<>(properties.length);
    -        for (int i = 0; i < properties.length; ++i) {
    -            PropertyParcel property = properties[i];
    +        Map propertyMap = new ArrayMap<>(properties.size());
    +        for (int i = 0; i < properties.size(); ++i) {
    +            PropertyParcel property = properties.get(i);
                 propertyMap.put(property.getPropertyName(), property);
             }
             return propertyMap;
    @@ -193,7 +208,7 @@
     
         /** Returns all the properties the document has. */
         @NonNull
    -    public PropertyParcel[] getProperties() {
    +    public List getProperties() {
             return mProperties;
         }
     
    @@ -203,6 +218,12 @@
             return mPropertyMap;
         }
     
    +    /** Returns the list of parent types for the {@link GenericDocument}. */
    +    @Nullable
    +    public List getParentTypes() {
    +        return mParentTypes;
    +    }
    +
         @Override
         public boolean equals(@Nullable Object other) {
             if (this == other) {
    @@ -218,8 +239,9 @@
                     && mTtlMillis == otherDocument.mTtlMillis
                     && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
                     && mScore == otherDocument.mScore
    -                && Arrays.equals(mProperties, otherDocument.mProperties)
    -                && Objects.equals(mPropertyMap, otherDocument.mPropertyMap);
    +                && Objects.equals(mProperties, otherDocument.mProperties)
    +                && Objects.equals(mPropertyMap, otherDocument.mPropertyMap)
    +                && Objects.equals(mParentTypes, otherDocument.mParentTypes);
         }
     
         @Override
    @@ -232,8 +254,9 @@
                         mTtlMillis,
                         mScore,
                         mCreationTimestampMillis,
    -                    Arrays.hashCode(mProperties),
    -                    mPropertyMap.hashCode());
    +                    Objects.hashCode(mProperties),
    +                    Objects.hashCode(mPropertyMap),
    +                    Objects.hashCode(mParentTypes));
             }
             return mHashCode;
         }
    @@ -252,7 +275,7 @@
             private long mTtlMillis;
             private int mScore;
             private Map mPropertyMap;
    -        private boolean mBuilt = false;
    +        @Nullable private List mParentTypes;
     
             /**
              * Creates a new {@link GenericDocument.Builder}.
    @@ -275,7 +298,6 @@
              * Creates a new {@link GenericDocumentParcel.Builder} from the given
              * {@link GenericDocumentParcel}.
              */
    -        @VisibleForTesting
             public Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
                 Objects.requireNonNull(documentSafeParcel);
     
    @@ -292,6 +314,10 @@
                 for (PropertyParcel value : propertyMap.values()) {
                     mPropertyMap.put(value.getPropertyName(), value);
                 }
    +
    +            // We don't need to create a shallow copy here, as in the setter for ParentTypes we
    +            // will create a new list anyway.
    +            mParentTypes = documentSafeParcel.mParentTypes;
             }
     
             /**
    @@ -306,7 +332,6 @@
             @NonNull
             public Builder setNamespace(@NonNull String namespace) {
                 Objects.requireNonNull(namespace);
    -            resetIfBuilt();
                 mNamespace = namespace;
                 return this;
             }
    @@ -321,7 +346,6 @@
             @NonNull
             public Builder setId(@NonNull String id) {
                 Objects.requireNonNull(id);
    -            resetIfBuilt();
                 mId = id;
                 return this;
             }
    @@ -336,7 +360,6 @@
             @NonNull
             public Builder setSchemaType(@NonNull String schemaType) {
                 Objects.requireNonNull(schemaType);
    -            resetIfBuilt();
                 mSchemaType = schemaType;
                 return this;
             }
    @@ -345,7 +368,6 @@
             @CanIgnoreReturnValue
             @NonNull
             public Builder setScore(int score) {
    -            resetIfBuilt();
                 mScore = score;
                 return this;
             }
    @@ -364,7 +386,6 @@
             @NonNull
             public Builder setCreationTimestampMillis(
                     /*@exportToFramework:CurrentTimeMillisLong*/ long creationTimestampMillis) {
    -            resetIfBuilt();
                 mCreationTimestampMillis = creationTimestampMillis;
                 return this;
             }
    @@ -388,12 +409,24 @@
                 if (ttlMillis < 0) {
                     throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
                 }
    -            resetIfBuilt();
                 mTtlMillis = ttlMillis;
                 return this;
             }
     
             /**
    +         * Sets the list of parent types of the {@link GenericDocument}'s type.
    +         *
    +         * 

    Child types must appear before parent types in the list. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setParentTypes(@NonNull List parentTypes) { + Objects.requireNonNull(parentTypes); + mParentTypes = new ArrayList<>(parentTypes); + return this; + } + + /** * Clears the value for the property with the given name. * *

    Note that this method does not support property paths. @@ -404,40 +437,43 @@ @NonNull public Builder clearProperty(@NonNull String name) { Objects.requireNonNull(name); - resetIfBuilt(); mPropertyMap.remove(name); return this; } /** puts an array of {@link String} in property map. */ + @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values) throws IllegalArgumentException { - mPropertyMap.put(name, + putInPropertyMap(name, new PropertyParcel.Builder(name).setStringValues(values).build()); return this; } /** puts an array of boolean in property map. */ + @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) { - mPropertyMap.put(name, + putInPropertyMap(name, new PropertyParcel.Builder(name).setBooleanValues(values).build()); return this; } /** puts an array of double in property map. */ + @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) { - mPropertyMap.put(name, + putInPropertyMap(name, new PropertyParcel.Builder(name).setDoubleValues(values).build()); return this; } /** puts an array of long in property map. */ + @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) { - mPropertyMap.put(name, + putInPropertyMap(name, new PropertyParcel.Builder(name).setLongValues(values).build()); return this; } @@ -445,26 +481,47 @@ /** * Converts and saves a byte[][] into {@link #mProperties}. */ + @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) { - mPropertyMap.put(name, + putInPropertyMap(name, new PropertyParcel.Builder(name).setBytesValues(values).build()); return this; } /** puts an array of {@link GenericDocumentParcel} in property map. */ + @CanIgnoreReturnValue @NonNull public Builder putInPropertyMap(@NonNull String name, @NonNull GenericDocumentParcel[] values) { - mPropertyMap.put(name, + putInPropertyMap(name, new PropertyParcel.Builder(name).setDocumentValues(values).build()); 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); + mPropertyMap.put(name, value); + return this; + } + /** Builds the {@link GenericDocument} object. */ @NonNull public GenericDocumentParcel build() { - mBuilt = true; // Set current timestamp for creation timestamp by default. if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) { mCreationTimestampMillis = System.currentTimeMillis(); @@ -476,19 +533,8 @@ mCreationTimestampMillis, mTtlMillis, mScore, - mPropertyMap.values().toArray(new PropertyParcel[0])); - } - - void resetIfBuilt() { - if (mBuilt) { - Map propertyMap = mPropertyMap; - mPropertyMap = new ArrayMap<>(propertyMap.size()); - for (PropertyParcel value : propertyMap.values()) { - // PropertyParcel is not deep copied since it is not mutable. - mPropertyMap.put(value.getPropertyName(), value); - } - mBuilt = false; - } + new ArrayList<>(mPropertyMap.values()), + mParentTypes); } } }

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java
    new file mode 100644
    index 0000000..98aee5b
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/GenericDocumentParcelCreator.java
    
    @@ -0,0 +1,142 @@
    +/*
    + * 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 androidx.appsearch.safeparcel;
    +
    +
    +import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +/**
    + * An implemented creator for {@link GenericDocumentParcel}.
    + *
    + * 

    In Jetpack, in order to serialize + * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument}, + * {@link PropertyParcel} needs to be a real {@link Parcelable}. + */ +// @exportToFramework:skipFile() +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class GenericDocumentParcelCreator implements + Parcelable.Creator { + private static final String PROPERTIES_FIELD = "properties"; + private static final String SCHEMA_TYPE_FIELD = "schemaType"; + private static final String ID_FIELD = "id"; + private static final String SCORE_FIELD = "score"; + private static final String TTL_MILLIS_FIELD = "ttlMillis"; + private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis"; + private static final String NAMESPACE_FIELD = "namespace"; + private static final String PARENT_TYPES_FIELD = "parentTypes"; + + /** Creates a {@link GenericDocumentParcel} from a {@link Bundle}. */ + @NonNull + private static GenericDocumentParcel createGenericDocumentParcelFromBundle( + @NonNull Bundle genericDocumentParcelBundle) { + // Get namespace, id, and schema type + String namespace = genericDocumentParcelBundle.getString(NAMESPACE_FIELD); + String id = genericDocumentParcelBundle.getString(ID_FIELD); + String schemaType = genericDocumentParcelBundle.getString(SCHEMA_TYPE_FIELD); + + // Those three can NOT be null. + if (namespace == null || id == null || schemaType == null) { + throw new IllegalArgumentException("GenericDocumentParcel bundle doesn't have " + + "namespace, id, or schemaType."); + } + + GenericDocumentParcel.Builder builder = new GenericDocumentParcel.Builder(namespace, + id, schemaType); + List parentTypes = + genericDocumentParcelBundle.getStringArrayList(PARENT_TYPES_FIELD); + if (parentTypes != null) { + builder.setParentTypes(parentTypes); + } + builder.setScore(genericDocumentParcelBundle.getInt(SCORE_FIELD)); + builder.setCreationTimestampMillis( + genericDocumentParcelBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD)); + builder.setTtlMillis(genericDocumentParcelBundle.getLong(TTL_MILLIS_FIELD)); + + // properties + Bundle propertyBundle = genericDocumentParcelBundle.getBundle(PROPERTIES_FIELD); + if (propertyBundle != null) { + for (String propertyName : propertyBundle.keySet()) { + // SuppressWarnings can be applied on a local variable, but not any + // single line of code. + @SuppressWarnings("deprecation") + PropertyParcel propertyParcel = propertyBundle.getParcelable(propertyName); + builder.putInPropertyMap(propertyName, propertyParcel); + } + } + + return builder.build(); + } + + /** Creates a {@link Bundle} from a {@link GenericDocumentParcel}. */ + @NonNull + private static Bundle createBundleFromGenericDocumentParcel( + @NonNull GenericDocumentParcel genericDocumentParcel) { + Bundle genericDocumentParcelBundle = new Bundle(); + + // Common fields + genericDocumentParcelBundle.putString(NAMESPACE_FIELD, + genericDocumentParcel.getNamespace()); + genericDocumentParcelBundle.putString(ID_FIELD, genericDocumentParcel.getId()); + genericDocumentParcelBundle.putString(SCHEMA_TYPE_FIELD, + genericDocumentParcel.getSchemaType()); + genericDocumentParcelBundle.putStringArrayList(PARENT_TYPES_FIELD, + (ArrayList) genericDocumentParcel.getParentTypes()); + genericDocumentParcelBundle.putInt(SCORE_FIELD, genericDocumentParcel.getScore()); + genericDocumentParcelBundle.putLong(CREATION_TIMESTAMP_MILLIS_FIELD, + genericDocumentParcel.getCreationTimestampMillis()); + genericDocumentParcelBundle.putLong(TTL_MILLIS_FIELD, + genericDocumentParcel.getTtlMillis()); + + // Properties + Bundle properties = new Bundle(); + List propertyParcels = genericDocumentParcel.getProperties(); + for (int i = 0; i < propertyParcels.size(); ++i) { + PropertyParcel propertyParcel = propertyParcels.get(i); + properties.putParcelable(propertyParcel.getPropertyName(), propertyParcel); + } + genericDocumentParcelBundle.putBundle(PROPERTIES_FIELD, properties); + + return genericDocumentParcelBundle; + } + + @Nullable + @Override + public GenericDocumentParcel createFromParcel(Parcel in) { + Bundle bundle = in.readBundle(getClass().getClassLoader()); + return createGenericDocumentParcelFromBundle(bundle); + } + + @Override + public GenericDocumentParcel[] newArray(int size) { + return new GenericDocumentParcel[size]; + } + + /** Writes a {@link GenericDocumentParcel} to a {@link Parcel}. */ + public static void writeToParcel(@NonNull GenericDocumentParcel genericDocumentParcel, + @NonNull android.os.Parcel parcel, int flags) { + parcel.writeBundle(createBundleFromGenericDocumentParcel(genericDocumentParcel)); + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java
    new file mode 100644
    index 0000000..f405f48
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcel.java
    
    @@ -0,0 +1,101 @@
    +/*
    + * 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 androidx.appsearch.safeparcel;
    +
    +import android.annotation.SuppressLint;
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.app.PackageIdentifier;
    +import androidx.appsearch.flags.FlaggedApi;
    +import androidx.appsearch.flags.Flags;
    +import androidx.core.util.Preconditions;
    +
    +import java.util.Arrays;
    +import java.util.Objects;
    +
    +/**
    + * Holds data for a {@link PackageIdentifier}.
    + *
    + * TODO(b/275592563): This class is currently used in GetSchemaResponse as a bundle, and
    + * therefore needs to implement Parcelable directly. Reassess if this is still needed once
    + * VisibilityConfig becomes available, and if not we should switch to a SafeParcelable
    + * implementation instead.
    + * @exportToFramework:hide
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    [email protected](creator = "PackageIdentifierParcelCreator")
    +@SuppressLint("BanParcelableUsage")
    +public final class PackageIdentifierParcel extends AbstractSafeParcelable implements Parcelable {
    +    @NonNull
    +    public static final Parcelable.Creator CREATOR =
    +            new PackageIdentifierParcelCreator();
    +
    +    @Field(id = 1, getter = "getPackageName")
    +    private final String mPackageName;
    +    @Field(id = 2, getter = "getSha256Certificate")
    +    private final byte[] mSha256Certificate;
    +
    +    /**
    +     * Creates a unique identifier for a package.
    +     *
    +     * @see PackageIdentifier
    +     */
    +    @Constructor
    +    public PackageIdentifierParcel(@Param(id = 1) @NonNull String packageName,
    +            @Param(id = 2) @NonNull byte[] sha256Certificate) {
    +        mPackageName = Preconditions.checkNotNull(packageName);
    +        mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
    +    }
    +
    +    @NonNull
    +    public String getPackageName() {
    +        return mPackageName;
    +    }
    +
    +    @NonNull
    +    public byte[] getSha256Certificate() {
    +        return mSha256Certificate;
    +    }
    +
    +    @Override
    +    public boolean equals(@Nullable Object obj) {
    +        if (this == obj) {
    +            return true;
    +        }
    +        if (!(obj instanceof PackageIdentifierParcel)) {
    +            return false;
    +        }
    +        final PackageIdentifierParcel other = (PackageIdentifierParcel) obj;
    +        return mPackageName.equals(other.mPackageName)
    +                && Arrays.equals(mSha256Certificate, other.mSha256Certificate);
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        return Objects.hash(mPackageName, Arrays.hashCode(mSha256Certificate));
    +    }
    +
    +    @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
    +    @Override
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        PackageIdentifierParcelCreator.writeToParcel(this, dest, flags);
    +    }
    +}
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java
    new file mode 100644
    index 0000000..16fe177
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PackageIdentifierParcelCreator.java
    
    @@ -0,0 +1,93 @@
    +/*
    + * 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 androidx.appsearch.safeparcel;
    +
    +import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
    +import androidx.core.util.Preconditions;
    +
    +import java.util.Objects;
    +
    +/**
    + * An implemented creator for {@link PackageIdentifierParcel}.
    + *
    + * 

    In Jetpack, {@link androidx.appsearch.app.PackageIdentifier} is serialized in a bundle for + * {@link androidx.appsearch.app.GetSchemaResponse}, and therefore needs to implement a real + * {@link Parcelable}. + */ +// @exportToFramework:skipFile() +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class PackageIdentifierParcelCreator implements Parcelable.Creator { + private static final String PACKAGE_NAME_FIELD = "packageName"; + private static final String SHA256_CERTIFICATE_FIELD = "sha256Certificate"; + + public PackageIdentifierParcelCreator() { + } + + /** + * Creates a {@link PackageIdentifierParcel} from a {@link Bundle} + */ + @NonNull + private static PackageIdentifierParcel createPackageIdentifierFromBundle( + @NonNull Bundle packageIdentifierBundle) { + Objects.requireNonNull(packageIdentifierBundle); + String packageName = + Preconditions.checkNotNull(packageIdentifierBundle.getString(PACKAGE_NAME_FIELD)); + byte[] sha256Certificate = + Preconditions.checkNotNull( + packageIdentifierBundle.getByteArray(SHA256_CERTIFICATE_FIELD)); + + return new PackageIdentifierParcel(packageName, sha256Certificate); + } + + /** Creates a {@link Bundle} from a {@link PackageIdentifierParcel}. */ + @NonNull + private static Bundle createBundleFromPackageIdentifier( + @NonNull PackageIdentifierParcel packageIdentifierParcel) { + Objects.requireNonNull(packageIdentifierParcel); + Bundle packageIdentifierBundle = new Bundle(); + packageIdentifierBundle.putString(PACKAGE_NAME_FIELD, + packageIdentifierParcel.getPackageName()); + packageIdentifierBundle.putByteArray(SHA256_CERTIFICATE_FIELD, + packageIdentifierParcel.getSha256Certificate()); + + return packageIdentifierBundle; + } + + @NonNull + @Override + public PackageIdentifierParcel createFromParcel(Parcel parcel) { + Bundle bundle = Preconditions.checkNotNull(parcel.readBundle(getClass().getClassLoader())); + return createPackageIdentifierFromBundle(bundle); + } + + @NonNull + @Override + public PackageIdentifierParcel[] newArray(int size) { + return new PackageIdentifierParcel[size]; + } + + /** Writes a {@link PackageIdentifierParcel} to a {@link Parcel}. */ + public static void writeToParcel(@NonNull PackageIdentifierParcel packageIdentifierParcel, + @NonNull android.os.Parcel parcel, int flags) { + parcel.writeBundle(createBundleFromPackageIdentifier(packageIdentifierParcel)); + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
    index 61029bd..96c53ae 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyConfigParcel.java
    
    @@ -17,6 +17,7 @@
     package androidx.appsearch.safeparcel;
     
     import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    @@ -27,10 +28,12 @@
     import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.JoinableValueType;
     import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig.TokenizerType;
     import androidx.appsearch.safeparcel.stub.StubCreators.DocumentIndexingConfigParcelCreator;
    +import androidx.appsearch.safeparcel.stub.StubCreators.EmbeddingIndexingConfigParcelCreator;
     import androidx.appsearch.safeparcel.stub.StubCreators.IntegerIndexingConfigParcelCreator;
     import androidx.appsearch.safeparcel.stub.StubCreators.JoinableConfigParcelCreator;
     import androidx.appsearch.safeparcel.stub.StubCreators.PropertyConfigParcelCreator;
     import androidx.appsearch.safeparcel.stub.StubCreators.StringIndexingConfigParcelCreator;
    +import androidx.core.util.ObjectsCompat;
     
     import java.util.List;
     import java.util.Objects;
    @@ -49,7 +52,8 @@
     @SafeParcelable.Class(creator = "PropertyConfigParcelCreator")
     public final class PropertyConfigParcel extends AbstractSafeParcelable {
         @NonNull
    -    public static final PropertyConfigParcelCreator CREATOR = new PropertyConfigParcelCreator();
    +    public static final Parcelable.Creator CREATOR =
    +            new PropertyConfigParcelCreator();
     
         @Field(id = 1, getter = "getName")
         private final String mName;
    @@ -63,23 +67,32 @@
         private final int mCardinality;
     
         @Field(id = 4, getter = "getSchemaType")
    -    private final String mSchemaType;
    +    @Nullable private final String mSchemaType;
     
         @Field(id = 5, getter = "getStringIndexingConfigParcel")
    -    private final StringIndexingConfigParcel mStringIndexingConfigParcel;
    +    @Nullable private final StringIndexingConfigParcel mStringIndexingConfigParcel;
     
         @Field(id = 6, getter = "getDocumentIndexingConfigParcel")
    -    private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
    +    @Nullable private final DocumentIndexingConfigParcel mDocumentIndexingConfigParcel;
     
         @Field(id = 7, getter = "getIntegerIndexingConfigParcel")
    -    private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
    +    @Nullable private final IntegerIndexingConfigParcel mIntegerIndexingConfigParcel;
     
         @Field(id = 8, getter = "getJoinableConfigParcel")
    -    private final JoinableConfigParcel mJoinableConfigParcel;
    +    @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}. */
         @Constructor
    -    public PropertyConfigParcel(
    +    PropertyConfigParcel(
                 @Param(id = 1) @NonNull String name,
                 @Param(id = 2) @DataType int dataType,
                 @Param(id = 3) @Cardinality int cardinality,
    @@ -87,7 +100,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;
    @@ -96,6 +111,147 @@
             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) {
    +        return new PropertyConfigParcel(
    +                Objects.requireNonNull(propertyName),
    +                AppSearchSchema.PropertyConfig.DATA_TYPE_STRING,
    +                cardinality,
    +                /*schemaType=*/ null,
    +                Objects.requireNonNull(stringIndexingConfigParcel),
    +                /*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,
    +                new IntegerIndexingConfigParcel(indexingType),
    +                /*joinableConfigParcel=*/ null,
    +                Objects.requireNonNull(description),
    +                /*embeddingIndexingConfigParcel=*/ null);
    +    }
    +
    +    /** Creates a {@link PropertyConfigParcel} for Double. */
    +    @NonNull
    +    public static PropertyConfigParcel createForDouble(
    +            @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,
    +                Objects.requireNonNull(description),
    +                /*embeddingIndexingConfigParcel=*/ null);
    +    }
    +
    +    /** Creates a {@link PropertyConfigParcel} for Boolean. */
    +    @NonNull
    +    public static PropertyConfigParcel createForBoolean(
    +            @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,
    +                Objects.requireNonNull(description),
    +                /*embeddingIndexingConfigParcel=*/ null);
    +    }
    +
    +    /** Creates a {@link PropertyConfigParcel} for Bytes. */
    +    @NonNull
    +    public static PropertyConfigParcel createForBytes(
    +            @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,
    +                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) {
    +        return new PropertyConfigParcel(
    +                Objects.requireNonNull(propertyName),
    +                AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT,
    +                cardinality,
    +                Objects.requireNonNull(schemaType),
    +                /*stringIndexingConfigParcel=*/ null,
    +                Objects.requireNonNull(documentIndexingConfigParcel),
    +                /*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. */
    @@ -104,6 +260,12 @@
             return mName;
         }
     
    +    /** Gets description for the property. */
    +    @NonNull
    +    public String getDescription() {
    +        return mDescription;
    +    }
    +
         /** Gets data type for the property. */
         @DataType
         public int getDataType() {
    @@ -146,11 +308,84 @@
             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);
    +    }
    +
    +    @Override
    +    public boolean equals(@Nullable Object other) {
    +        if (this == other) {
    +            return true;
    +        }
    +        if (!(other instanceof PropertyConfigParcel)) {
    +            return false;
    +        }
    +        PropertyConfigParcel otherProperty = (PropertyConfigParcel) other;
    +        return ObjectsCompat.equals(mName, otherProperty.mName)
    +                && Objects.equals(mDescription, otherProperty.mDescription)
    +                && ObjectsCompat.equals(mDataType, otherProperty.mDataType)
    +                && ObjectsCompat.equals(mCardinality, otherProperty.mCardinality)
    +                && ObjectsCompat.equals(mSchemaType, otherProperty.mSchemaType)
    +                && ObjectsCompat.equals(
    +                mStringIndexingConfigParcel, otherProperty.mStringIndexingConfigParcel)
    +                && ObjectsCompat.equals(
    +                mDocumentIndexingConfigParcel, otherProperty.mDocumentIndexingConfigParcel)
    +                && ObjectsCompat.equals(
    +                mIntegerIndexingConfigParcel, otherProperty.mIntegerIndexingConfigParcel)
    +                && ObjectsCompat.equals(
    +                mJoinableConfigParcel, otherProperty.mJoinableConfigParcel)
    +                && ObjectsCompat.equals(
    +                mEmbeddingIndexingConfigParcel, otherProperty.mEmbeddingIndexingConfigParcel);
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        if (mHashCode == null) {
    +            mHashCode =
    +                ObjectsCompat.hash(
    +                        mName,
    +                        mDescription,
    +                        mDataType,
    +                        mCardinality,
    +                        mSchemaType,
    +                        mStringIndexingConfigParcel,
    +                        mDocumentIndexingConfigParcel,
    +                        mIntegerIndexingConfigParcel,
    +                        mJoinableConfigParcel,
    +                        mEmbeddingIndexingConfigParcel);
    +        }
    +        return mHashCode;
    +    }
    +
    +    @Override
    +    @NonNull
    +    public String toString() {
    +        return "{name: " + mName
    +                + ", description: " + mDescription
    +                + ", dataType: " + mDataType
    +                + ", cardinality: " + mCardinality
    +                + ", schemaType: " + mSchemaType
    +                + ", stringIndexingConfigParcel: " + mStringIndexingConfigParcel
    +                + ", documentIndexingConfigParcel: " + mDocumentIndexingConfigParcel
    +                + ", integerIndexingConfigParcel: " + mIntegerIndexingConfigParcel
    +                + ", joinableConfigParcel: " + mJoinableConfigParcel
    +                + ", embeddingIndexingConfigParcel: " + mEmbeddingIndexingConfigParcel
    +                + "}";
    +    }
    +
         /** Class to hold join configuration for a String type. */
         @SafeParcelable.Class(creator = "JoinableConfigParcelCreator")
         public static class JoinableConfigParcel extends AbstractSafeParcelable {
             @NonNull
    -        public static final JoinableConfigParcelCreator CREATOR = new JoinableConfigParcelCreator();
    +        public static final Parcelable.Creator CREATOR =
    +                new JoinableConfigParcelCreator();
     
             @JoinableValueType
             @Field(id = 1, getter = "getJoinableValueType")
    @@ -183,13 +418,38 @@
             public void writeToParcel(@NonNull Parcel dest, int flags) {
                 JoinableConfigParcelCreator.writeToParcel(this, dest, flags);
             }
    +
    +        @Override
    +        public int hashCode() {
    +            return ObjectsCompat.hash(mJoinableValueType, mDeletionPropagation);
    +        }
    +
    +        @Override
    +        public boolean equals(@Nullable Object other) {
    +            if (this == other) {
    +                return true;
    +            }
    +            if (!(other instanceof JoinableConfigParcel)) {
    +                return false;
    +            }
    +            JoinableConfigParcel otherObject = (JoinableConfigParcel) other;
    +            return ObjectsCompat.equals(mJoinableValueType, otherObject.mJoinableValueType)
    +                    && ObjectsCompat.equals(mDeletionPropagation, otherObject.mDeletionPropagation);
    +        }
    +
    +        @Override
    +        @NonNull
    +        public String toString() {
    +            return "{joinableValueType: " + mJoinableValueType
    +                    + ", deletePropagation " + mDeletionPropagation + "}";
    +        }
         }
     
         /** Class to hold configuration a string type. */
         @SafeParcelable.Class(creator = "StringIndexingConfigParcelCreator")
         public static class StringIndexingConfigParcel extends AbstractSafeParcelable {
             @NonNull
    -        public static final StringIndexingConfigParcelCreator CREATOR =
    +        public static final Parcelable.Creator CREATOR =
                     new StringIndexingConfigParcelCreator();
     
             @AppSearchSchema.StringPropertyConfig.IndexingType
    @@ -225,13 +485,38 @@
             public void writeToParcel(@NonNull Parcel dest, int flags) {
                 StringIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
             }
    +
    +        @Override
    +        public int hashCode() {
    +            return ObjectsCompat.hash(mIndexingType, mTokenizerType);
    +        }
    +
    +        @Override
    +        public boolean equals(@Nullable Object other) {
    +            if (this == other) {
    +                return true;
    +            }
    +            if (!(other instanceof StringIndexingConfigParcel)) {
    +                return false;
    +            }
    +            StringIndexingConfigParcel otherObject = (StringIndexingConfigParcel) other;
    +            return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType)
    +                    && ObjectsCompat.equals(mTokenizerType, otherObject.mTokenizerType);
    +        }
    +
    +        @Override
    +        @NonNull
    +        public String toString() {
    +            return "{indexingType: " + mIndexingType
    +                    + ", tokenizerType " + mTokenizerType + "}";
    +        }
         }
     
         /** Class to hold configuration for integer property type. */
         @SafeParcelable.Class(creator = "IntegerIndexingConfigParcelCreator")
         public static class IntegerIndexingConfigParcel extends AbstractSafeParcelable {
             @NonNull
    -        public static final IntegerIndexingConfigParcelCreator CREATOR =
    +        public static final Parcelable.Creator CREATOR =
                     new IntegerIndexingConfigParcelCreator();
     
             @AppSearchSchema.LongPropertyConfig.IndexingType
    @@ -255,13 +540,36 @@
             public void writeToParcel(@NonNull Parcel dest, int flags) {
                 IntegerIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
             }
    +
    +        @Override
    +        public int hashCode() {
    +            return ObjectsCompat.hashCode(mIndexingType);
    +        }
    +
    +        @Override
    +        public boolean equals(@Nullable Object other) {
    +            if (this == other) {
    +                return true;
    +            }
    +            if (!(other instanceof IntegerIndexingConfigParcel)) {
    +                return false;
    +            }
    +            IntegerIndexingConfigParcel otherObject = (IntegerIndexingConfigParcel) other;
    +            return ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType);
    +        }
    +
    +        @Override
    +        @NonNull
    +        public String toString() {
    +            return "{indexingType: " + mIndexingType + "}";
    +        }
         }
     
         /** Class to hold configuration for document property type. */
         @SafeParcelable.Class(creator = "DocumentIndexingConfigParcelCreator")
         public static class DocumentIndexingConfigParcel extends AbstractSafeParcelable {
             @NonNull
    -        public static final DocumentIndexingConfigParcelCreator CREATOR =
    +        public static final Parcelable.Creator CREATOR =
                     new DocumentIndexingConfigParcelCreator();
     
             @Field(id = 1, getter = "shouldIndexNestedProperties")
    @@ -295,10 +603,86 @@
             public void writeToParcel(@NonNull Parcel dest, int flags) {
                 DocumentIndexingConfigParcelCreator.writeToParcel(this, dest, flags);
             }
    +
    +        @Override
    +        public int hashCode() {
    +            return ObjectsCompat.hash(mIndexNestedProperties, mIndexableNestedPropertiesList);
    +        }
    +
    +        @Override
    +        public boolean equals(@Nullable Object other) {
    +            if (this == other) {
    +                return true;
    +            }
    +            if (!(other instanceof DocumentIndexingConfigParcel)) {
    +                return false;
    +            }
    +            DocumentIndexingConfigParcel otherObject = (DocumentIndexingConfigParcel) other;
    +            return ObjectsCompat.equals(mIndexNestedProperties, otherObject.mIndexNestedProperties)
    +                    && ObjectsCompat.equals(mIndexableNestedPropertiesList,
    +                    otherObject.mIndexableNestedPropertiesList);
    +        }
    +
    +        @Override
    +        @NonNull
    +        public String toString() {
    +            return "{indexNestedProperties: " + mIndexNestedProperties
    +                    + ", indexableNestedPropertiesList: " + mIndexableNestedPropertiesList
    +                    + "}";
    +        }
         }
     
    -    @Override
    -    public void writeToParcel(@NonNull Parcel dest, int flags) {
    -        PropertyConfigParcelCreator.writeToParcel(this, dest, flags);
    +    /** Class to hold configuration for embedding property. */
    +    @SafeParcelable.Class(creator = "EmbeddingIndexingConfigParcelCreator")
    +    public static class EmbeddingIndexingConfigParcel extends AbstractSafeParcelable {
    +        @NonNull
    +        public static final Parcelable.Creator 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 ObjectsCompat.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 ObjectsCompat.equals(mIndexingType, otherObject.mIndexingType);
    +        }
    +
    +        @Override
    +        @NonNull
    +        public String toString() {
    +            return "{indexingType: " + mIndexingType + "}";
    +        }
         }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
    index 3762adc..3e4fb3a 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcel.java
    
    @@ -17,12 +17,15 @@
     package androidx.appsearch.safeparcel;
     
     
    +import android.annotation.SuppressLint;
     import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
    -import androidx.appsearch.safeparcel.stub.StubCreators.PropertyParcelCreator;
    +import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.app.EmbeddingVector;
     
     import java.util.Arrays;
     import java.util.Objects;
    @@ -36,8 +39,11 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @SafeParcelable.Class(creator = "PropertyParcelCreator")
    -public final class PropertyParcel extends AbstractSafeParcelable {
    -    @NonNull public static final PropertyParcelCreator CREATOR = new PropertyParcelCreator();
    +// 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 Parcelable.Creator CREATOR =
    +            new PropertyParcelCreator();
     
         @NonNull
         @Field(id = 1, getter = "getPropertyName")
    @@ -67,6 +73,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
    @@ -77,7 +87,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;
    @@ -85,6 +96,7 @@
             mBooleanValues = booleanValues;
             mBytesValues = bytesValues;
             mDocumentValues = documentValues;
    +        mEmbeddingValues = embeddingValues;
             checkOnlyOneArrayCanBeSet();
         }
     
    @@ -130,6 +142,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.
          *
    @@ -155,6 +173,9 @@
             if (mDocumentValues != null) {
                 return mDocumentValues;
             }
    +        if (mEmbeddingValues != null) {
    +            return mEmbeddingValues;
    +        }
             return null;
         }
     
    @@ -183,6 +204,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");
    @@ -205,6 +229,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);
             }
    @@ -228,7 +254,13 @@
                     && 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
    +    public void writeToParcel(@NonNull Parcel dest, int flags) {
    +        PropertyParcelCreator.writeToParcel(this, dest, flags);
         }
     
         /** Builder for {@link PropertyParcel}. */
    @@ -240,12 +272,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);
    @@ -253,6 +287,7 @@
             }
     
             /** Sets long values. */
    +        @CanIgnoreReturnValue
             @NonNull
             public Builder setLongValues(@NonNull long[] longValues) {
                 mLongValues = Objects.requireNonNull(longValues);
    @@ -260,6 +295,7 @@
             }
     
             /** Sets double values. */
    +        @CanIgnoreReturnValue
             @NonNull
             public Builder setDoubleValues(@NonNull double[] doubleValues) {
                 mDoubleValues = Objects.requireNonNull(doubleValues);
    @@ -267,6 +303,7 @@
             }
     
             /** Sets boolean values. */
    +        @CanIgnoreReturnValue
             @NonNull
             public Builder setBooleanValues(@NonNull boolean[] booleanValues) {
                 mBooleanValues = Objects.requireNonNull(booleanValues);
    @@ -274,6 +311,7 @@
             }
     
             /** Sets a two dimension byte array. */
    +        @CanIgnoreReturnValue
             @NonNull
             public Builder setBytesValues(@NonNull byte[][] bytesValues) {
                 mBytesValues = Objects.requireNonNull(bytesValues);
    @@ -281,12 +319,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() {
    @@ -297,12 +344,8 @@
                         mDoubleValues,
                         mBooleanValues,
                         mBytesValues,
    -                    mDocumentValues);
    +                    mDocumentValues,
    +                    mEmbeddingValues);
             }
         }
    -
    -    @Override
    -    public void writeToParcel(@NonNull Parcel dest, int flags) {
    -        PropertyParcelCreator.writeToParcel(this, dest, flags);
    -    }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java
    new file mode 100644
    index 0000000..46be473
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/PropertyParcelCreator.java
    
    @@ -0,0 +1,223 @@
    +/*
    + * 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 androidx.appsearch.safeparcel;
    +
    +import android.os.Bundle;
    +import android.os.Parcel;
    +import android.os.Parcelable;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.app.EmbeddingVector;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +import java.util.Objects;
    +
    +/**
    + * An implemented creator for {@link PropertyParcel}.
    + *
    + * 

    In Jetpack, in order to serialize + * {@link GenericDocumentParcel} for {@link androidx.appsearch.app.GenericDocument}, + * {@link PropertyParcel} needs to be a real {@link Parcelable}. + */ +// @exportToFramework:skipFile() +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class PropertyParcelCreator implements Parcelable.Creator { + private static final String PROPERTY_NAME_FIELD = "propertyName"; + private static final String STRING_ARRAY_FIELD = "stringArray"; + private static final String LONG_ARRAY_FIELD = "longArray"; + private static final String DOUBLE_ARRAY_FIELD = "doubleArray"; + private static final String BOOLEAN_ARRAY_FIELD = "booleanArray"; + // 1d + private static final String BYTE_ARRAY_FIELD = "byteArray"; + // 2d + private static final String BYTES_ARRAY_FIELD = "bytesArray"; + private static final String DOC_ARRAY_FIELD = "docArray"; + private static final String EMBEDDING_VALUE_FIELD = "embeddingValue"; + private static final String EMBEDDING_MODEL_SIGNATURE_FIELD = "embeddingModelSignature"; + private static final String EMBEDDING_ARRAY_FIELD = "embeddingArray"; + + public PropertyParcelCreator() { + } + + /** Creates a {@link PropertyParcel} from a {@link Bundle}. */ + @SuppressWarnings({"unchecked"}) + @NonNull + private static PropertyParcel createPropertyParcelFromBundle( + @NonNull Bundle propertyParcelBundle) { + Objects.requireNonNull(propertyParcelBundle); + String propertyName = propertyParcelBundle.getString(PROPERTY_NAME_FIELD); + + Objects.requireNonNull(propertyName); + PropertyParcel.Builder builder = new PropertyParcel.Builder(propertyName); + + // Get the values out of the bundle. + String[] stringValues = propertyParcelBundle.getStringArray(STRING_ARRAY_FIELD); + long[] longValues = propertyParcelBundle.getLongArray(LONG_ARRAY_FIELD); + double[] doubleValues = propertyParcelBundle.getDoubleArray(DOUBLE_ARRAY_FIELD); + boolean[] booleanValues = propertyParcelBundle.getBooleanArray(BOOLEAN_ARRAY_FIELD); + + List bytesArray; + // SuppressWarnings can be applied on a local variable, but not any single line of + // code. + @SuppressWarnings("deprecation") + List tmpList = propertyParcelBundle.getParcelableArrayList(BYTES_ARRAY_FIELD); + bytesArray = tmpList; + + Parcelable[] docValues; + // SuppressWarnings can be applied on a local variable, but not any single line of + // code. + @SuppressWarnings("deprecation") + Parcelable[] tmpParcel = propertyParcelBundle.getParcelableArray(DOC_ARRAY_FIELD); + docValues = tmpParcel; + + // SuppressWarnings can be applied on a local variable, but not any single line of + // code. + @SuppressWarnings("deprecation") + List embeddingArray = propertyParcelBundle.getParcelableArrayList( + EMBEDDING_ARRAY_FIELD); + + // Only one of those values will be set. + boolean valueSet = false; + if (stringValues != null) { + builder.setStringValues(stringValues); + valueSet = true; + } else if (longValues != null) { + builder.setLongValues(longValues); + valueSet = true; + } else if (doubleValues != null) { + builder.setDoubleValues(doubleValues); + valueSet = true; + } else if (booleanValues != null) { + builder.setBooleanValues(booleanValues); + valueSet = true; + } else if (bytesArray != null) { + byte[][] bytes = new byte[bytesArray.size()][]; + for (int i = 0; i < bytesArray.size(); i++) { + Bundle byteArray = bytesArray.get(i); + if (byteArray == null) { + continue; + } + byte[] innerBytes = byteArray.getByteArray(BYTE_ARRAY_FIELD); + if (innerBytes == null) { + continue; + } + bytes[i] = innerBytes; + } + builder.setBytesValues(bytes); + valueSet = true; + } else if (docValues != null && docValues.length > 0) { + GenericDocumentParcel[] documentParcels = + new GenericDocumentParcel[docValues.length]; + System.arraycopy(docValues, 0, documentParcels, 0, docValues.length); + builder.setDocumentValues(documentParcels); + valueSet = true; + } else if (embeddingArray != null) { + EmbeddingVector[] embeddings = new EmbeddingVector[embeddingArray.size()]; + for (int i = 0; i < embeddingArray.size(); i++) { + Bundle embeddingBundle = embeddingArray.get(i); + if (embeddingBundle == null) { + continue; + } + float[] values = embeddingBundle.getFloatArray(EMBEDDING_VALUE_FIELD); + String modelSignature = embeddingBundle.getString(EMBEDDING_MODEL_SIGNATURE_FIELD); + if (values == null || modelSignature == null) { + continue; + } + embeddings[i] = new EmbeddingVector(values, modelSignature); + } + builder.setEmbeddingValues(embeddings); + valueSet = true; + } + + if (!valueSet) { + throw new IllegalArgumentException("property bundle passed in doesn't have any " + + "value set."); + } + + return builder.build(); + } + + /** Creates a {@link Bundle} from a {@link PropertyParcel}. */ + @NonNull + private static Bundle createBundleFromPropertyParcel( + @NonNull PropertyParcel propertyParcel) { + Objects.requireNonNull(propertyParcel); + Bundle propertyParcelBundle = new Bundle(); + propertyParcelBundle.putString(PROPERTY_NAME_FIELD, propertyParcel.getPropertyName()); + + // Check and set the properties + String[] stringValues = propertyParcel.getStringValues(); + long[] longValues = propertyParcel.getLongValues(); + double[] doubleValues = propertyParcel.getDoubleValues(); + boolean[] booleanValues = propertyParcel.getBooleanValues(); + byte[][] bytesArray = propertyParcel.getBytesValues(); + GenericDocumentParcel[] docArray = propertyParcel.getDocumentValues(); + EmbeddingVector[] embeddingArray = propertyParcel.getEmbeddingValues(); + + if (stringValues != null) { + propertyParcelBundle.putStringArray(STRING_ARRAY_FIELD, stringValues); + } else if (longValues != null) { + propertyParcelBundle.putLongArray(LONG_ARRAY_FIELD, longValues); + } else if (doubleValues != null) { + propertyParcelBundle.putDoubleArray(DOUBLE_ARRAY_FIELD, doubleValues); + } else if (booleanValues != null) { + propertyParcelBundle.putBooleanArray(BOOLEAN_ARRAY_FIELD, booleanValues); + } else if (bytesArray != null) { + ArrayList bundles = new ArrayList<>(bytesArray.length); + for (int i = 0; i < bytesArray.length; i++) { + Bundle byteArray = new Bundle(); + byteArray.putByteArray(BYTE_ARRAY_FIELD, bytesArray[i]); + bundles.add(byteArray); + } + propertyParcelBundle.putParcelableArrayList(BYTES_ARRAY_FIELD, bundles); + } else if (docArray != null) { + propertyParcelBundle.putParcelableArray(DOC_ARRAY_FIELD, docArray); + } else if (embeddingArray != null) { + ArrayList bundles = new ArrayList<>(embeddingArray.length); + for (int i = 0; i < embeddingArray.length; i++) { + Bundle embedding = new Bundle(); + embedding.putFloatArray(EMBEDDING_VALUE_FIELD, embeddingArray[i].getValues()); + embedding.putString(EMBEDDING_MODEL_SIGNATURE_FIELD, + embeddingArray[i].getModelSignature()); + bundles.add(embedding); + } + propertyParcelBundle.putParcelableArrayList(EMBEDDING_ARRAY_FIELD, bundles); + } + + return propertyParcelBundle; + } + + @NonNull + @Override + public PropertyParcel createFromParcel(Parcel in) { + Bundle bundle = in.readBundle(getClass().getClassLoader()); + return createPropertyParcelFromBundle(bundle); + } + + @Override + public PropertyParcel[] newArray(int size) { + return new PropertyParcel[size]; + } + + /** Writes a {@link PropertyParcel} to a {@link Parcel}. */ + public static void writeToParcel(@NonNull PropertyParcel propertyParcel, + @NonNull android.os.Parcel parcel, int flags) { + parcel.writeBundle(createBundleFromPropertyParcel(propertyParcel)); + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
    index fa70911..8a25a36 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/SafeParcelable.java
    
    @@ -74,4 +74,7 @@
              */
             boolean doNotParcelTypeDefaultValues() default false;
         }
    +
    +    /** Provide same interface as {@link android.os.Parcelable} for code sync purpose. */
    +    int describeContents();
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
    index 22755d0..53afaa7 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/AbstractCreator.java
    
    @@ -17,6 +17,7 @@
     package androidx.appsearch.safeparcel.stub;
     
     import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.RestrictTo;
    @@ -31,7 +32,21 @@
      */
     // @exportToFramework:skipFile()
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    -abstract class AbstractCreator {
    +abstract class AbstractCreator implements Parcelable.Creator {
    +    @Override
    +    public T createFromParcel(Parcel var1) {
    +        // This is here only for code sync purpose.
    +        throw new UnsupportedOperationException("createFromParcel is not implemented and should "
    +                + "not be used.");
    +    }
    +
    +    @Override
    +    public T[] newArray(int var1) {
    +        // This is here only for code sync purpose.
    +        throw new UnsupportedOperationException("newArray is not implemented and should "
    +                + "not be used.");
    +    }
    +
         public static void writeToParcel(
                 @NonNull SafeParcelable safeParcelable,
                 @NonNull Parcel parcel,
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
    index 97887bf..48d197c 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/safeparcel/stub/StubCreators.java
    
    @@ -16,9 +16,34 @@
     package androidx.appsearch.safeparcel.stub;
     
     import androidx.annotation.RestrictTo;
    -import androidx.appsearch.safeparcel.GenericDocumentParcel;
    +import androidx.appsearch.app.AppSearchSchema;
    +import androidx.appsearch.app.EmbeddingVector;
    +import androidx.appsearch.app.GetByDocumentIdRequest;
    +import androidx.appsearch.app.GetSchemaResponse;
    +import androidx.appsearch.app.InternalSetSchemaResponse;
    +import androidx.appsearch.app.InternalVisibilityConfig;
    +import androidx.appsearch.app.JoinSpec;
    +import androidx.appsearch.app.RemoveByDocumentIdRequest;
    +import androidx.appsearch.app.ReportUsageRequest;
    +import androidx.appsearch.app.SchemaVisibilityConfig;
    +import androidx.appsearch.app.SearchResult;
    +import androidx.appsearch.app.SearchResult.MatchInfo;
    +import androidx.appsearch.app.SearchResultPage;
    +import androidx.appsearch.app.SearchSpec;
    +import androidx.appsearch.app.SearchSuggestionResult;
    +import androidx.appsearch.app.SearchSuggestionSpec;
    +import androidx.appsearch.app.SetSchemaResponse;
    +import androidx.appsearch.app.SetSchemaResponse.MigrationFailure;
    +import androidx.appsearch.app.StorageInfo;
    +import androidx.appsearch.app.VisibilityPermissionConfig;
    +import androidx.appsearch.observer.ObserverSpec;
     import androidx.appsearch.safeparcel.PropertyConfigParcel;
    -import androidx.appsearch.safeparcel.PropertyParcel;
    +import androidx.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel;
    +import androidx.appsearch.safeparcel.PropertyConfigParcel.EmbeddingIndexingConfigParcel;
    +import androidx.appsearch.safeparcel.PropertyConfigParcel.IntegerIndexingConfigParcel;
    +import androidx.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel;
    +import androidx.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel;
    +import androidx.appsearch.stats.SchemaMigrationStats;
     
     /**
      * Stub creators for any classes extending
    @@ -29,61 +54,145 @@
      * be provided for code sync purpose.
      */
     // @exportToFramework:skipFile()
    -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
     public class StubCreators {
         /** Stub creator for {@link androidx.appsearch.app.StorageInfo}. */
    -    public static class StorageInfoCreator extends AbstractCreator {
    -    }
    -
    -    /** Stub creator for {@link PropertyParcel}. */
    -    public static class PropertyParcelCreator extends AbstractCreator {
    +    public static class StorageInfoCreator extends AbstractCreator {
         }
     
         /** Stub creator for {@link PropertyConfigParcel}. */
    -    public static class PropertyConfigParcelCreator extends AbstractCreator {
    +    public static class PropertyConfigParcelCreator extends AbstractCreator {
         }
     
         /**
          * Stub creator for
          * {@link PropertyConfigParcel.JoinableConfigParcel}.
          */
    -    public static class JoinableConfigParcelCreator extends AbstractCreator {
    +    public static class JoinableConfigParcelCreator extends AbstractCreator {
         }
     
         /**
          * Stub creator for
          * {@link PropertyConfigParcel.StringIndexingConfigParcel}.
          */
    -    public static class StringIndexingConfigParcelCreator extends AbstractCreator {
    +    public static class StringIndexingConfigParcelCreator extends
    +            AbstractCreator {
         }
     
         /**
          * Stub creator for
          * {@link PropertyConfigParcel.IntegerIndexingConfigParcel}.
          */
    -    public static class IntegerIndexingConfigParcelCreator extends AbstractCreator {
    +    public static class IntegerIndexingConfigParcelCreator extends
    +            AbstractCreator {
         }
     
         /**
          * Stub creator for
          * {@link PropertyConfigParcel.DocumentIndexingConfigParcel}.
          */
    -    public static class DocumentIndexingConfigParcelCreator extends AbstractCreator {
    +    public static class DocumentIndexingConfigParcelCreator extends
    +            AbstractCreator {
         }
     
    -    /** Stub creator for {@link GenericDocumentParcel}. */
    -    public static class GenericDocumentParcelCreator extends AbstractCreator {
    +    /** Stub creator for {@link SchemaVisibilityConfig}. */
    +    public static class VisibilityConfigCreator extends AbstractCreator {
         }
     
    -    /** Stub creator for {@link androidx.appsearch.app.VisibilityPermissionDocument}. */
    -    public static class VisibilityPermissionDocumentCreator extends AbstractCreator {
    +    /**
    +     * Stub creator for {@link EmbeddingIndexingConfigParcel}.
    +     */
    +    public static class EmbeddingIndexingConfigParcelCreator extends
    +            AbstractCreator {
         }
     
    -    /** Stub creator for {@link androidx.appsearch.app.VisibilityDocument}. */
    -    public static class VisibilityDocumentCreator extends AbstractCreator {
    +    /** Stub creator for {@link InternalVisibilityConfig}. */
    +    public static class InternalVisibilityConfigCreator
    +            extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link VisibilityPermissionConfig}. */
    +    public static class VisibilityPermissionConfigCreator extends
    +            AbstractCreator {
         }
     
         /** Stub creator for {@link androidx.appsearch.stats.SchemaMigrationStats}. */
    -    public static class SchemaMigrationStatsCreator extends AbstractCreator {
    +    public static class SchemaMigrationStatsCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SearchSuggestionResult}. */
    +    public static class SearchSuggestionResultCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SearchSuggestionSpec}. */
    +    public static class SearchSuggestionSpecCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.observer.ObserverSpec}. */
    +    public static class ObserverSpecCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SetSchemaResponse}. */
    +    public static class SetSchemaResponseCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SetSchemaResponse.MigrationFailure}. */
    +    public static class MigrationFailureCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.InternalSetSchemaResponse}. */
    +    public static class InternalSetSchemaResponseCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SearchSpec}. */
    +    public static class SearchSpecCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.JoinSpec}. */
    +    public static class JoinSpecCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.GetSchemaResponse}. */
    +    public static class GetSchemaResponseCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.AppSearchSchema}. */
    +    public static class AppSearchSchemaCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SearchResult}. */
    +    public static class SearchResultCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.MatchInfo}. */
    +    public static class MatchInfoCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.SearchResultPage}. */
    +    public static class SearchResultPageCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.RemoveByDocumentIdRequest}. */
    +    public static class RemoveByDocumentIdRequestCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.ReportUsageRequest}. */
    +    public static class ReportUsageRequestCreator extends AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link androidx.appsearch.app.GetByDocumentIdRequest}. */
    +    public static class GetByDocumentIdRequestCreator extends
    +            AbstractCreator {
    +    }
    +
    +    /** Stub creator for {@link EmbeddingVector}. */
    +    public static class EmbeddingVectorCreator extends
    +            AbstractCreator {
         }
     }
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
    index 0a24293..91b74e7 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/stats/SchemaMigrationStats.java
    
    @@ -17,6 +17,7 @@
     package androidx.appsearch.stats;
     
     import android.os.Parcel;
    +import android.os.Parcelable;
     
     import androidx.annotation.IntDef;
     import androidx.annotation.NonNull;
    @@ -40,10 +41,10 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @SafeParcelable.Class(creator = "SchemaMigrationStatsCreator")
     public final class SchemaMigrationStats extends AbstractSafeParcelable {
    -    @NonNull public static final SchemaMigrationStatsCreator CREATOR =
    +    @NonNull public static final Parcelable.Creator 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,
    
    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java
    new file mode 100644
    index 0000000..0fadd2c
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ActionConstants.java
    
    @@ -0,0 +1,55 @@
    +/*
    + * 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 androidx.appsearch.usagereporting;
    +
    +import androidx.annotation.RestrictTo;
    +
    +/**
    + * Wrapper class for action constants.
    + *
    + * @exportToFramework:hide
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +public final class ActionConstants {
    +    /**
    +     * Unknown action type.
    +     *
    +     * 

    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. + * + * + *

    It is the action type for {@link SearchAction}. + * + */ + public static final int ACTION_TYPE_SEARCH = 1; + + /** + * Click action type. + * + * + *

    It is the action type for {@link ClickAction}. + * + */ + public static final int ACTION_TYPE_CLICK = 2; + + private ActionConstants() {} +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java
    new file mode 100644
    index 0000000..e69320a
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/ClickAction.java
    
    @@ -0,0 +1,286 @@
    +/*
    + * 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.
    + */
    +// @exportToFramework:skipFile()
    +
    +package androidx.appsearch.usagereporting;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RequiresFeature;
    +import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.annotation.Document;
    +import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
    +import androidx.appsearch.app.Features;
    +import androidx.core.util.Preconditions;
    +
    +/**
    + * {@link ClickAction} is a built-in AppSearch document type that contains different metrics.
    + * Clients can report the user's click actions on a {@link androidx.appsearch.app.SearchResult}
    + * document.
    + *
    + * 

    In order to use this document type, the client must explicitly set this schema type via + * {@link androidx.appsearch.app.SetSchemaRequest.Builder#addDocumentClasses}. + * + *

    Click actions can be used as signals to boost ranking via + * {@link androidx.appsearch.app.JoinSpec} API in future search requests. + * + *

    Since {@link ClickAction} is an AppSearch document, the client can handle deletion via + * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The + * default TTL is 60 days. + */ +// In ClickAction document, there is a joinable property "referencedQualifiedId" for reporting the +// qualified id of the clicked document. The client can create personal navboost with click action +// signals by join query with this property. Therefore, ClickAction document class requires join +// feature. +@RequiresFeature( + enforcement = "androidx.appsearch.app.Features#isFeatureSupported", + name = Features.JOIN_SPEC_AND_QUALIFIED_ID) +@Document(name = "builtin:ClickAction") +public class ClickAction extends TakenAction { + @Nullable + @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES) + private final String mQuery; + + @Nullable + @Document.StringProperty(joinableValueType = + StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID) + private final String mReferencedQualifiedId; + + @Document.LongProperty + private final int mResultRankInBlock; + + @Document.LongProperty + private final int mResultRankGlobal; + + @Document.LongProperty + private final long mTimeStayOnResultMillis; + + ClickAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis, + long actionTimestampMillis, @TakenAction.ActionType int actionType, + @Nullable String query, @Nullable String referencedQualifiedId, int resultRankInBlock, + int resultRankGlobal, long timeStayOnResultMillis) { + super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType); + + mQuery = query; + mReferencedQualifiedId = referencedQualifiedId; + mResultRankInBlock = resultRankInBlock; + mResultRankGlobal = resultRankGlobal; + mTimeStayOnResultMillis = timeStayOnResultMillis; + } + + /** + * Returns the user-entered search input (without any operators or rewriting) that yielded the + * {@link androidx.appsearch.app.SearchResult} on which the user clicked. + */ + @Nullable + public String getQuery() { + return mQuery; + } + + /** + * Returns the qualified id of the {@link androidx.appsearch.app.SearchResult} document that the + * user clicked on. + * + *

    A qualified id is a string generated by package, database, namespace, and document id. See + * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId(String,String,String,String)} + * for more details. + */ + @Nullable + public String getReferencedQualifiedId() { + return mReferencedQualifiedId; + } + + /** + * Returns the rank of the {@link androidx.appsearch.app.SearchResult} document among the + * user-defined block. + * + *

    The client can define its own custom definition for block, e.g. corpus name, group, etc. + * + *

    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]. + * + *

    If the client is not presenting the results in multiple blocks, they should set this value + * to match {@link #getResultRankGlobal}. + * + *

    If unset, then the block rank of the {@link androidx.appsearch.app.SearchResult} document + * will be set to -1 to mark invalid. + */ + public int getResultRankInBlock() { + return mResultRankInBlock; + } + + /** + * Returns the global rank of the {@link androidx.appsearch.app.SearchResult} document. + * + *

    Global rank reflects the order of {@link androidx.appsearch.app.SearchResult} documents + * returned by AppSearch. + * + *

    For example, AppSearch returns 2 pages with 10 {@link androidx.appsearch.app.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. + * + *

    If unset, then the global rank of the {@link androidx.appsearch.app.SearchResult} document + * will be set to -1 to mark invalid. + */ + public int getResultRankGlobal() { + return mResultRankGlobal; + } + + /** + * Returns the time in milliseconds that user stays on the + * {@link androidx.appsearch.app.SearchResult} document after clicking it. + */ + public long getTimeStayOnResultMillis() { + return mTimeStayOnResultMillis; + } + + // TODO(b/314026345): redesign builder to enable inheritance for ClickAction. + /** Builder for {@link ClickAction}. */ + @Document.BuilderProducer + public static final class Builder extends BuilderImpl { + private String mQuery; + private String mReferencedQualifiedId; + private int mResultRankInBlock; + private int mResultRankGlobal; + private long mTimeStayOnResultMillis; + + /** + * Constructor for {@link ClickAction.Builder}. + * + * @param namespace Namespace for the Document. See {@link Document.Namespace}. + * @param id Unique identifier for the Document. See {@link Document.Id}. + * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds + * since Unix epoch. + */ + public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) { + this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_CLICK); + } + + /** + * Constructs {@link ClickAction.Builder} by copying existing values from the given + * {@link ClickAction}. + * + * @param clickAction an existing {@link ClickAction} object. + */ + public Builder(@NonNull ClickAction clickAction) { + super(Preconditions.checkNotNull(clickAction)); + + mQuery = clickAction.getQuery(); + mReferencedQualifiedId = clickAction.getReferencedQualifiedId(); + mResultRankInBlock = clickAction.getResultRankInBlock(); + mResultRankGlobal = clickAction.getResultRankGlobal(); + mTimeStayOnResultMillis = clickAction.getTimeStayOnResultMillis(); + } + + /** + * Constructor for {@link ClickAction.Builder}. + * + *

    It is required by {@link Document.BuilderProducer}. + * + * @param namespace Namespace for the Document. See {@link Document.Namespace}. + * @param id Unique identifier for the Document. See {@link Document.Id}. + * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds + * since Unix epoch. + * @param actionType Action type enum for the Document. See + * {@link TakenAction.ActionType}. + */ + Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis, + @TakenAction.ActionType int actionType) { + super(namespace, id, actionTimestampMillis, actionType); + + // Default for unset result rank fields. Since negative number is invalid for ranking, + // -1 is used as an unset value and AppSearch will ignore it. + mResultRankInBlock = -1; + mResultRankGlobal = -1; + + // Default for unset timeStayOnResultMillis. Since negative number is invalid for + // time in millis, -1 is used as an unset value and AppSearch will ignore it. + mTimeStayOnResultMillis = -1; + } + + /** + * Sets the user-entered search input (without any operators or rewriting) that yielded + * the {@link androidx.appsearch.app.SearchResult} on which the user clicked. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setQuery(@Nullable String query) { + mQuery = query; + return this; + } + + /** + * Sets the qualified id of the {@link androidx.appsearch.app.SearchResult} document that + * the user takes action on. + * + *

    A qualified id is a string generated by package, database, namespace, and document id. + * See {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId( + * String,String,String,String)} for more details. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setReferencedQualifiedId(@Nullable String referencedQualifiedId) { + mReferencedQualifiedId = referencedQualifiedId; + return this; + } + + /** + * Sets the rank of the {@link androidx.appsearch.app.SearchResult} document among the + * user-defined block. + * + * @see ClickAction#getResultRankInBlock + */ + @CanIgnoreReturnValue + @NonNull + public Builder setResultRankInBlock(int resultRankInBlock) { + mResultRankInBlock = resultRankInBlock; + return this; + } + + /** + * Sets the global rank of the {@link androidx.appsearch.app.SearchResult} document. + * + * @see ClickAction#getResultRankGlobal + */ + @CanIgnoreReturnValue + @NonNull + public Builder setResultRankGlobal(int resultRankGlobal) { + mResultRankGlobal = resultRankGlobal; + return this; + } + + /** + * Sets the time in milliseconds that user stays on the + * {@link androidx.appsearch.app.SearchResult} document after clicking it. + */ + @CanIgnoreReturnValue + @NonNull + public Builder setTimeStayOnResultMillis(long timeStayOnResultMillis) { + mTimeStayOnResultMillis = timeStayOnResultMillis; + return this; + } + + /** Builds a {@link ClickAction}. */ + @Override + @NonNull + public ClickAction build() { + return new ClickAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis, + mActionType, mQuery, mReferencedQualifiedId, mResultRankInBlock, + mResultRankGlobal, mTimeStayOnResultMillis); + } + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java
    new file mode 100644
    index 0000000..67cb4ea
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/SearchAction.java
    
    @@ -0,0 +1,155 @@
    +/*
    + * 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.
    + */
    +// @exportToFramework:skipFile()
    +
    +package androidx.appsearch.usagereporting;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.annotation.Document;
    +import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
    +import androidx.core.util.Preconditions;
    +
    +/**
    + * {@link SearchAction} is a built-in AppSearch document type that contains different metrics.
    + * 
      + *
    • Clients can report the user's search actions. + *
    • Usually {@link SearchAction} is reported together with {@link ClickAction}, since the + * user clicks on {@link androidx.appsearch.app.SearchResult} documents after searching. + *
    + * + *

    In order to use this document type, the client must explicitly set this schema type via + * {@link androidx.appsearch.app.SetSchemaRequest.Builder#addDocumentClasses}. + * + *

    Since {@link SearchAction} is an AppSearch document, the client can handle deletion via + * {@link androidx.appsearch.app.AppSearchSession#removeAsync} or document time-to-live (TTL). The + * default TTL is 60 days. + */ +@Document(name = "builtin:SearchAction") +public class SearchAction extends TakenAction { + @Nullable + @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES) + private final String mQuery; + + @Document.LongProperty + private final int mFetchedResultCount; + + SearchAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis, + long actionTimestampMillis, @TakenAction.ActionType int actionType, + @Nullable String query, int fetchedResultCount) { + super(namespace, id, documentTtlMillis, actionTimestampMillis, actionType); + + mQuery = query; + mFetchedResultCount = fetchedResultCount; + } + + /** Returns the user-entered search input (without any operators or rewriting). */ + @Nullable + public String getQuery() { + return mQuery; + } + + /** + * Returns total number of results fetched from AppSearch by the client in this + * {@link SearchAction}. + * + *

    If unset, then it will be set to -1 to mark invalid. + */ + public int getFetchedResultCount() { + return mFetchedResultCount; + } + + // TODO(b/314026345): redesign builder to enable inheritance for SearchAction. + /** Builder for {@link SearchAction}. */ + @Document.BuilderProducer + public static final class Builder extends BuilderImpl { + private String mQuery; + private int mFetchedResultCount; + + /** + * Constructor for {@link SearchAction.Builder}. + * + * @param namespace Namespace for the Document. See {@link Document.Namespace}. + * @param id Unique identifier for the Document. See {@link Document.Id}. + * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds + * since Unix epoch. + */ + public Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis) { + this(namespace, id, actionTimestampMillis, ActionConstants.ACTION_TYPE_SEARCH); + } + + /** + * Constructor for {@link Builder} with all the existing values. + */ + public Builder(@NonNull SearchAction searchAction) { + super(Preconditions.checkNotNull(searchAction)); + + mQuery = searchAction.getQuery(); + mFetchedResultCount = searchAction.getFetchedResultCount(); + } + + /** + * Constructor for {@link SearchAction.Builder}. + * + *

    It is required by {@link Document.BuilderProducer}. + * + * @param namespace Namespace for the Document. See {@link Document.Namespace}. + * @param id Unique identifier for the Document. See {@link Document.Id}. + * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds + * since Unix epoch. + * @param actionType Action type enum for the Document. See + * {@link TakenAction.ActionType}. + */ + Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis, + @TakenAction.ActionType int actionType) { + super(namespace, id, actionTimestampMillis, actionType); + + // Default for unset fetchedResultCount. Since negative number is invalid for fetched + // result count, -1 is used as an unset value and AppSearch will ignore it. + mFetchedResultCount = -1; + } + + /** Sets the user-entered search input (without any operators or rewriting). */ + @CanIgnoreReturnValue + @NonNull + public Builder setQuery(@Nullable String query) { + mQuery = query; + return this; + } + + /** + * Sets total number of results fetched from AppSearch by the client in this + * {@link SearchAction}. + * + * @see SearchAction#getFetchedResultCount + */ + @CanIgnoreReturnValue + @NonNull + public Builder setFetchedResultCount(int fetchedResultCount) { + mFetchedResultCount = fetchedResultCount; + return this; + } + + /** Builds a {@link SearchAction}. */ + @Override + @NonNull + public SearchAction build() { + return new SearchAction(mNamespace, mId, mDocumentTtlMillis, mActionTimestampMillis, + mActionType, mQuery, mFetchedResultCount); + } + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java
    new file mode 100644
    index 0000000..da35743
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/usagereporting/TakenAction.java
    
    @@ -0,0 +1,231 @@
    +/*
    + * 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.
    + */
    +// @exportToFramework:skipFile()
    +
    +package androidx.appsearch.usagereporting;
    +
    +import androidx.annotation.IntDef;
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
    +import androidx.appsearch.annotation.CanIgnoreReturnValue;
    +import androidx.appsearch.annotation.Document;
    +import androidx.core.util.Preconditions;
    +
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +
    +/**
    + * {@link TakenAction} is an abstract class which holds common fields of other AppSearch built-in
    + * action types (e.g. {@link SearchAction}, {@link ClickAction}).
    + *
    + * 

    Clients can report the user's actions by creating concrete actions with + * {@link androidx.appsearch.app.PutDocumentsRequest.Builder#addTakenActions} API. + */ +@Document(name = "builtin:TakenAction") +public abstract class TakenAction { + /** Default TTL for all related {@link TakenAction} documents: 60 days. */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static final long DEFAULT_DOCUMENT_TTL_MILLIS = 60L * 24 * 60 * 60 * 1000; + + /** AppSearch taken action type. */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + @IntDef(value = { + ActionConstants.ACTION_TYPE_UNKNOWN, + ActionConstants.ACTION_TYPE_SEARCH, + ActionConstants.ACTION_TYPE_CLICK, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ActionType { + } + + @NonNull + @Document.Namespace + private final String mNamespace; + + @NonNull + @Document.Id + private final String mId; + + @Document.TtlMillis + private final long mDocumentTtlMillis; + + @Document.CreationTimestampMillis + private final long mActionTimestampMillis; + + @Document.LongProperty + @ActionType + private final int mActionType; + + TakenAction(@NonNull String namespace, @NonNull String id, long documentTtlMillis, + long actionTimestampMillis, @ActionType int actionType) { + mNamespace = Preconditions.checkNotNull(namespace); + mId = Preconditions.checkNotNull(id); + mDocumentTtlMillis = documentTtlMillis; + mActionTimestampMillis = actionTimestampMillis; + mActionType = actionType; + } + + /** Returns the namespace of the {@link TakenAction}. */ + @NonNull + public String getNamespace() { + return mNamespace; + } + + /** Returns the unique identifier of the {@link TakenAction}. */ + @NonNull + public String getId() { + return mId; + } + + /** + * Returns the time-to-live (TTL) of the {@link TakenAction} document as a duration in + * milliseconds. + * + *

    The document will be automatically deleted when the TTL expires (since + * {@link #getActionTimestampMillis()}). + * + *

    The default TTL for {@link TakenAction} document is 60 days. + * + *

    See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on TTL. + */ + public long getDocumentTtlMillis() { + return mDocumentTtlMillis; + } + + /** + * Returns the timestamp when the user took the action, in milliseconds since Unix epoch. + * + *

    The action timestamp will be used together with {@link #getDocumentTtlMillis()} as the + * document retention. + */ + public long getActionTimestampMillis() { + return mActionTimestampMillis; + } + + /** + * Returns the action type of the {@link TakenAction}. + * + * @see TakenAction.ActionType + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @ActionType + public int getActionType() { + return mActionType; + } + + // TODO(b/330777270): improve AnnotationProcessor for abstract document class, and remove this + // builder. + /** Builder for {@link TakenAction}. */ + @Document.BuilderProducer + static final class Builder extends BuilderImpl { + /** + * Constructor for {@link TakenAction.Builder}. + * + * @param namespace Namespace for the Document. See {@link Document.Namespace}. + * @param id Unique identifier for the Document. See {@link Document.Id}. + * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds + * since Unix epoch. + * @param actionType Action type enum for the Document. See + * {@link TakenAction.ActionType}. + */ + Builder(@NonNull String namespace, @NonNull String id, long actionTimestampMillis, + @TakenAction.ActionType int actionType) { + super(namespace, id, actionTimestampMillis, actionType); + } + + /** Constructor for {@link TakenAction.Builder} with all the existing values. */ + Builder(@NonNull TakenAction takenAction) { + super(takenAction); + } + } + + // Use templated BuilderImpl to resolve base class setter return type issue for child class + // builder instances. + @SuppressWarnings("unchecked") + static class BuilderImpl> { + protected final String mNamespace; + protected final String mId; + protected long mDocumentTtlMillis; + protected long mActionTimestampMillis; + @ActionType + protected int mActionType; + + /** + * Constructs {@link TakenAction.BuilderImpl} with given {@code namespace}, {@code id}, + * {@code actionTimestampMillis} and {@code actionType}. + * + * @param namespace The namespace of the {@link TakenAction} document. + * @param id The id of the {@link TakenAction} document. + * @param actionTimestampMillis The timestamp when the user took the action, in milliseconds + * since Unix epoch. + * @param actionType The action type enum of the Document. + */ + BuilderImpl(@NonNull String namespace, @NonNull String id, long actionTimestampMillis, + @TakenAction.ActionType int actionType) { + mNamespace = Preconditions.checkNotNull(namespace); + mId = Preconditions.checkNotNull(id); + mActionTimestampMillis = actionTimestampMillis; + mActionType = actionType; + + // Default for documentTtlMillis. + mDocumentTtlMillis = TakenAction.DEFAULT_DOCUMENT_TTL_MILLIS; + } + + /** + * Constructs {@link TakenAction.BuilderImpl} by copying existing values from the given + * {@link TakenAction}. + * + * @param takenAction an existing {@link TakenAction} object. + */ + BuilderImpl(@NonNull TakenAction takenAction) { + this(takenAction.getNamespace(), takenAction.getId(), + takenAction.getActionTimestampMillis(), takenAction.getActionType()); + mDocumentTtlMillis = takenAction.getDocumentTtlMillis(); + } + + /** + * Sets the time-to-live (TTL) of the {@link TakenAction} document as a duration in + * milliseconds. + * + *

    The document will be automatically deleted when the TTL expires (since + * {@link TakenAction#getActionTimestampMillis()}). + * + *

    The default TTL for {@link TakenAction} document is 60 days. + * + *

    See {@link androidx.appsearch.annotation.Document.TtlMillis} for more information on + * TTL. + */ + @CanIgnoreReturnValue + @NonNull + public T setDocumentTtlMillis(long documentTtlMillis) { + mDocumentTtlMillis = documentTtlMillis; + return (T) this; + } + + // TODO(b/330777270): improve AnnotationProcessor for abstract document class builder, and + // make it an abstract method. + /** + * For AppSearch annotation processor requirement only. The client should never call it + * since it is impossible to instantiate an abstract class. + * + * @throws UnsupportedOperationException + */ + @NonNull + public TakenAction build() { + throw new UnsupportedOperationException(); + } + } +}

    diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
    index 9d50086..7604db5 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
    
    @@ -248,7 +248,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/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java
    new file mode 100644
    index 0000000..c09ac7a
    --- /dev/null
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/ExceptionUtil.java
    
    @@ -0,0 +1,67 @@
    +/*
    + * 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 androidx.appsearch.util;
    +
    +import android.os.RemoteException;
    +import android.util.Log;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
    +
    +/**
    + * Utilities for handling exceptions.
    + */
    +// This file has different behavior in Framework as compared to JetPack (like it is okay to rethrow
    +// exception and log instead of rethrowing from SystemServer in case of RemoteException). This file
    +// is not synced to Framework, as it maintains its own environment specific copy.
    +// @exportToFramework:skipFile()
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +public final class ExceptionUtil {
    +    private static final String TAG = "AppSearchExceptionUtil";
    +
    +    /**
    +     * {@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 true;
    +    }
    +
    +    /** Rethrow exception from SystemServer in Framework code. */
    +    public static void handleRemoteException(@NonNull RemoteException e) {
    +        Log.w(TAG, "Unable to make a call to AppSearchManagerService!", e);
    +    }
    +
    +    /**
    +     * A helper method to rethrow {@link RuntimeException}.
    +     *
    +     * 

    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/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
    index cc52cdd..0f3e100 100644
    --- a/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
    +++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/LogUtil.java
    
    @@ -22,6 +22,7 @@
     import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
     import androidx.annotation.Size;
    +import androidx.appsearch.app.AppSearchEnvironmentFactory;
     
     /**
      * Utilities for logging to logcat.
    @@ -33,6 +34,8 @@
         // TODO(b/232285376): If it becomes possible to detect an eng build, turn this on by default
         //  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
    @@ -92,7 +95,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/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
    index dd8ae1d..39dd6c7 100644
    --- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AnnotatedGetterOrField.java
    
    @@ -527,6 +527,13 @@
                             env,
                             /* allowRepeated= */true);
                     break;
    +            case EMBEDDING_PROPERTY:
    +                requireTypeIsOneOf(
    +                        getterOrField,
    +                        List.of(helper.mEmbeddingType),
    +                        env,
    +                        /* allowRepeated= */true);
    +                break;
                 default:
                     throw new IllegalStateException("Unhandled annotation: " + annotation);
             }
    
    diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
    index 9f11508..7296edd 100644
    --- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
    
    @@ -203,8 +203,9 @@
             //       unboxing.
             //
             //   1b: ListCallArraysAsList
    -        //       List contains String. We have to convert this from an array of String[], but no
    -        //       conversion of the collection elements is needed. We can use Arrays#asList for this.
    +        //       List contains String or EmbeddingVector. We have to convert this from
    +        //       an array of String[] or EmbeddingVector[], but no conversion of the
    +        //       collection elements is needed. We can use Arrays#asList for this.
             //
             //   1c: ListForLoopCallFromGenericDocument
             //       List contains a class which is annotated with @Document.
    @@ -225,7 +226,8 @@
             //       of unboxing.
             //
             //   2b: ArrayUseDirectly
    -        //       Array is of type String[], long[], double[], boolean[], byte[][].
    +        //       Array is of type String[], long[], double[], boolean[], byte[][] or
    +        //       EmbeddingVector[].
             //       We can directly use this field with no conversion.
             //
             //   2c: ArrayForLoopCallFromGenericDocument
    @@ -243,7 +245,8 @@
     
             // Scenario 3: Single valued fields
             //   3a: FieldUseDirectlyWithNullCheck
    -        //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
    +        //       Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
    +        //       EmbeddingVector.
             //       We can use this field directly, after testing for null. The java compiler will box
             //       or unbox as needed.
             //
    @@ -393,6 +396,19 @@
                         default:
                             throw new IllegalStateException("Unhandled type-category: " + typeCategory);
                     }
    +            case EMBEDDING_PROPERTY:
    +                switch (typeCategory) {
    +                    case COLLECTION: // List: 1b
    +                        return listCallArraysAsList(annotation, getterOrField);
    +                    case ARRAY:
    +                        // EmbeddingVector[]: 2b
    +                        return arrayUseDirectly(annotation, getterOrField);
    +                    case SINGLE:
    +                        // EmbeddingVector: 3a
    +                        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
    +                    default:
    +                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
    +                }
                 default:
                     throw new IllegalStateException("Unhandled annotation: " + annotation);
             }
    @@ -433,8 +449,9 @@
         }
     
         // 1b: ListCallArraysAsList
    -    //     List contains String. We have to convert this from an array of String[], but no
    -    //     conversion of the collection elements is needed. We can use Arrays#asList for this.
    +    //     List contains String or EmbeddingVector. We have to convert this from
    +    //     an array of String[] or EmbeddingVector[], but no conversion of the
    +    //     collection elements is needed. We can use Arrays#asList for this.
         @NonNull
         private CodeBlock listCallArraysAsList(
                 @NonNull DataPropertyAnnotation annotation,
    @@ -559,8 +576,8 @@
         }
     
         // 2b: ArrayUseDirectly
    -    //     Array is of type String[], long[], double[], boolean[], byte[][].
    -    //     We can directly use this field with no conversion.
    +    //     Array is of type String[], long[], double[], boolean[], byte[][] or
    +    //     EmbeddingVector[].
         @NonNull
         private CodeBlock arrayUseDirectly(
                 @NonNull DataPropertyAnnotation annotation,
    @@ -646,7 +663,8 @@
         }
     
         // 3a: FieldUseDirectlyWithNullCheck
    -    //     Field is of type String, Long, Integer, Double, Float, Boolean, byte[].
    +    //     Field is of type String, Long, Integer, Double, Float, Boolean, byte[] or
    +    //     EmbeddingVector.
         //     We can use this field directly, after testing for null. The java compiler will box
         //     or unbox as needed.
         @NonNull
    
    diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
    index 8ad865d..dba768a 100644
    --- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
    
    @@ -90,6 +90,9 @@
         public static final ClassName GENERIC_DOCUMENT_CLASS =
                 ClassName.get(APPSEARCH_PKG, "GenericDocument");
     
    +    public static final ClassName EMBEDDING_VECTOR_CLASS =
    +            ClassName.get(APPSEARCH_PKG, "EmbeddingVector");
    +
         public static final ClassName BUILDER_PRODUCER_CLASS =
                 DOCUMENT_ANNOTATION_CLASS.nestedClass("BuilderProducer");
     
    @@ -108,6 +111,7 @@
         public final TypeMirror mBooleanPrimitiveType;
         public final TypeMirror mBytePrimitiveArrayType;
         public final TypeMirror mGenericDocumentType;
    +    public final TypeMirror mEmbeddingType;
         public final TypeMirror mDoublePrimitiveType;
         final TypeMirror mCollectionType;
         final TypeMirror mListType;
    @@ -151,6 +155,8 @@
             mBytePrimitiveArrayType = mTypeUtils.getArrayType(mBytePrimitiveType);
             mGenericDocumentType =
                     mElementUtils.getTypeElement(GENERIC_DOCUMENT_CLASS.canonicalName()).asType();
    +        mEmbeddingType = mElementUtils.getTypeElement(
    +                EMBEDDING_VECTOR_CLASS.canonicalName()).asType();
         }
     
         /**
    
    diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
    index 9199c47..42c6b31 100644
    --- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
    
    @@ -28,6 +28,7 @@
     import androidx.annotation.NonNull;
     import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
     import androidx.appsearch.compiler.annotationwrapper.DocumentPropertyAnnotation;
    +import androidx.appsearch.compiler.annotationwrapper.EmbeddingPropertyAnnotation;
     import androidx.appsearch.compiler.annotationwrapper.LongPropertyAnnotation;
     import androidx.appsearch.compiler.annotationwrapper.StringPropertyAnnotation;
     
    @@ -229,6 +230,12 @@
                     LongPropertyAnnotation longPropertyAnnotation = (LongPropertyAnnotation) annotation;
                     codeBlock.add(createSetIndexingTypeExpr(longPropertyAnnotation, getterOrField));
                     break;
    +            case EMBEDDING_PROPERTY:
    +                EmbeddingPropertyAnnotation embeddingPropertyAnnotation =
    +                        (EmbeddingPropertyAnnotation) annotation;
    +                codeBlock.add(
    +                        createSetIndexingTypeExpr(embeddingPropertyAnnotation, getterOrField));
    +                break;
                 case DOUBLE_PROPERTY: // fall-through
                 case BOOLEAN_PROPERTY: // fall-through
                 case BYTES_PROPERTY:
    @@ -418,6 +425,31 @@
     
         /**
          * Creates an expr like
    +     * {@code .setIndexingType(EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)}.
    +     */
    +    @NonNull
    +    private static CodeBlock createSetIndexingTypeExpr(
    +            @NonNull EmbeddingPropertyAnnotation annotation,
    +            @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
    +        String enumName;
    +        switch (annotation.getIndexingType()) {
    +            case 0:
    +                enumName = "INDEXING_TYPE_NONE";
    +                break;
    +            case 1:
    +                enumName = "INDEXING_TYPE_SIMILARITY";
    +                break;
    +            default:
    +                throw new ProcessingException(
    +                        "Unknown indexing type " + annotation.getIndexingType(),
    +                        getterOrField.getElement());
    +        }
    +        return CodeBlock.of("\n.setIndexingType($T.$N)",
    +                EmbeddingPropertyAnnotation.CONFIG_CLASS, enumName);
    +    }
    +
    +    /**
    +     * Creates an expr like
          * {@code .setJoinableValueType(StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID)}.
          */
         @NonNull
    
    diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
    index 96bba93..90ebb78 100644
    --- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
    
    @@ -165,10 +165,10 @@
             //       care of unboxing and widening where necessary.
             //
             //   1b: CollectionCallToArray
    -        //       Collection contains String or GenericDocument.
    -        //       We have to convert this into an array of String[] or GenericDocument[], but no
    -        //       conversion of the collection elements is needed. We can use Collection#toArray for
    -        //       this.
    +        //       Collection contains String, GenericDocument or EmbeddingVector.
    +        //       We have to convert this into an array of String[], GenericDocument[] or
    +        //       EmbeddingVector[], but no conversion of the collection elements is
    +        //       needed. We can use Collection#toArray for this.
             //
             //   1c: CollectionForLoopCallToGenericDocument
             //       Collection contains a class which is annotated with @Document.
    @@ -188,8 +188,8 @@
             //       unboxing and widening where necessary.
             //
             //   2b: ArrayUseDirectly
    -        //       Array is of type String[], long[], double[], boolean[], byte[][] or
    -        //       GenericDocument[].
    +        //       Array is of type String[], long[], double[], boolean[], byte[][],
    +        //       GenericDocument[] or EmbeddingVector[].
             //       We can directly use this field with no conversion.
             //
             //   2c: ArrayForLoopCallToGenericDocument
    @@ -207,7 +207,8 @@
     
             // Scenario 3: Single valued fields
             //   3a: FieldUseDirectlyWithNullCheck
    -        //       Field is of type String, Long, Integer, Double, Float, Boolean.
    +        //       Field is of type String, Long, Integer, Double, Float, Boolean or
    +        //       EmbeddingVector.
             //       We can use this field directly, after testing for null. The java compiler will box
             //       or unbox as needed.
             //
    @@ -375,6 +376,20 @@
                         default:
                             throw new IllegalStateException("Unhandled type-category: " + typeCategory);
                     }
    +            case EMBEDDING_PROPERTY:
    +                switch (typeCategory) {
    +                    case COLLECTION:
    +                        // List: 1b
    +                        return collectionCallToArray(annotation, getterOrField);
    +                    case ARRAY:
    +                        // EmbeddingVector[]: 2b
    +                        return arrayUseDirectly(annotation, getterOrField);
    +                    case SINGLE:
    +                        // EmbeddingVector: 3a
    +                        return fieldUseDirectlyWithNullCheck(annotation, getterOrField);
    +                    default:
    +                        throw new IllegalStateException("Unhandled type-category: " + typeCategory);
    +                }
                 default:
                     throw new IllegalStateException("Unhandled annotation: " + annotation);
             }
    @@ -417,10 +432,10 @@
         }
     
         // 1b: CollectionCallToArray
    -    //     Collection contains String or GenericDocument.
    -    //     We have to convert this into an array of String[] or GenericDocument[], but no
    -    //     conversion of the collection elements is needed. We can use Collection#toArray for
    -    //     this.
    +    //     Collection contains String, GenericDocument or EmbeddingVector.
    +    //     We have to convert this into an array of String[], GenericDocument[] or
    +    //     EmbeddingVector[], but no conversion of the collection elements is
    +    //     needed. We can use Collection#toArray for this.
         @NonNull
         private CodeBlock collectionCallToArray(
                 @NonNull DataPropertyAnnotation annotation,
    @@ -536,8 +551,8 @@
         }
     
         // 2b: ArrayUseDirectly
    -    //     Array is of type String[], long[], double[], boolean[], byte[][] or
    -    //     GenericDocument[].
    +    //     Array is of type String[], long[], double[], boolean[], byte[][],
    +    //     GenericDocument[] or EmbeddingVector[].
         //     We can directly use this field with no conversion.
         @NonNull
         private CodeBlock arrayUseDirectly(
    @@ -613,7 +628,8 @@
         }
     
         // 3a: FieldUseDirectlyWithNullCheck
    -    //     Field is of type String, Long, Integer, Double, Float, Boolean.
    +    //     Field is of type String, Long, Integer, Double, Float, Boolean or
    +    //     EmbeddingVector.
         //     We can use this field directly, after testing for null. The java compiler will box
         //     or unbox as needed.
         @NonNull
    
    diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
    index 38621c4..1c78f19 100644
    --- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/DataPropertyAnnotation.java
    
    @@ -38,12 +38,13 @@
      *     
  • {@link DoublePropertyAnnotation}
  • *
  • {@link BooleanPropertyAnnotation}
  • *
  • {@link BytesPropertyAnnotation}
  • + *
  • {@link EmbeddingPropertyAnnotation}
  • * */ public abstract class DataPropertyAnnotation implements PropertyAnnotation { public enum Kind { STRING_PROPERTY, DOCUMENT_PROPERTY, LONG_PROPERTY, DOUBLE_PROPERTY, BOOLEAN_PROPERTY, - BYTES_PROPERTY + BYTES_PROPERTY, EMBEDDING_PROPERTY } @NonNull @@ -103,6 +104,9 @@ return LongPropertyAnnotation.parse(annotationParams, defaultName); } else if (qualifiedClassName.equals(StringPropertyAnnotation.CLASS_NAME.canonicalName())) { return StringPropertyAnnotation.parse(annotationParams, defaultName); + } else if (qualifiedClassName.equals( + EmbeddingPropertyAnnotation.CLASS_NAME.canonicalName())) { + return EmbeddingPropertyAnnotation.parse(annotationParams, defaultName); } return null; }
    diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java
    new file mode 100644
    index 0000000..882c737
    --- /dev/null
    +++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/annotationwrapper/EmbeddingPropertyAnnotation.java
    
    @@ -0,0 +1,85 @@
    +/*
    + * 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 androidx.appsearch.compiler.annotationwrapper;
    +
    +import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_SCHEMA_CLASS;
    +import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS;
    +
    +import androidx.annotation.NonNull;
    +import androidx.appsearch.compiler.IntrospectionHelper;
    +import androidx.appsearch.compiler.ProcessingException;
    +
    +import com.google.auto.value.AutoValue;
    +import com.squareup.javapoet.ClassName;
    +
    +import java.util.Map;
    +
    +import javax.lang.model.type.TypeMirror;
    +
    +/**
    + * An instance of the {@code @Document.EmbeddingProperty} annotation.
    + */
    +@AutoValue
    +public abstract class EmbeddingPropertyAnnotation extends DataPropertyAnnotation {
    +    public static final ClassName CLASS_NAME =
    +            DOCUMENT_ANNOTATION_CLASS.nestedClass("EmbeddingProperty");
    +
    +    public static final ClassName CONFIG_CLASS =
    +            APPSEARCH_SCHEMA_CLASS.nestedClass("EmbeddingPropertyConfig");
    +
    +    public EmbeddingPropertyAnnotation() {
    +        super(
    +                CLASS_NAME,
    +                CONFIG_CLASS,
    +                /* genericDocGetterName= */"getPropertyEmbedding",
    +                /* genericDocArrayGetterName= */"getPropertyEmbeddingArray",
    +                /* genericDocSetterName= */"setPropertyEmbedding");
    +    }
    +
    +    /**
    +     * @param defaultName The name to use for the annotated property in case the annotation
    +     *                    params do not mention an explicit name.
    +     * @throws ProcessingException If the annotation points to an Illegal serializer class.
    +     */
    +    @NonNull
    +    static EmbeddingPropertyAnnotation parse(
    +            @NonNull Map annotationParams,
    +            @NonNull String defaultName) throws ProcessingException {
    +        String name = (String) annotationParams.get("name");
    +        return new AutoValue_EmbeddingPropertyAnnotation(
    +                name.isEmpty() ? defaultName : name,
    +                (boolean) annotationParams.get("required"),
    +                (int) annotationParams.get("indexingType"));
    +    }
    +
    +    /**
    +     * Specifies how a property should be indexed.
    +     */
    +    public abstract int getIndexingType();
    +
    +    @NonNull
    +    @Override
    +    public final Kind getDataPropertyKind() {
    +        return Kind.EMBEDDING_PROPERTY;
    +    }
    +
    +    @NonNull
    +    @Override
    +    public TypeMirror getUnderlyingTypeWithinGenericDoc(@NonNull IntrospectionHelper helper) {
    +        return helper.mEmbeddingType;
    +    }
    +}
    
    diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
    index 57cbd28..fa7b7aa 100644
    --- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
    +++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
    
    @@ -1048,18 +1048,20 @@
             // TODO(b/156296904): Uncomment Gift in this test when it's supported
             Compilation compilation = compile(
                     "import java.util.*;\n"
    +                        + "import androidx.appsearch.app.EmbeddingVector;\n"
                             + "@Document\n"
                             + "public class Gift {\n"
                             + "  @Document.Namespace String namespace;\n"
                             + "  @Document.Id String id;\n"
    -                        + "  @Document.StringProperty String stringProp;\n"
    -                        + "  @Document.LongProperty Integer integerProp;\n"
    -                        + "  @Document.LongProperty Long longProp;\n"
    -                        + "  @Document.DoubleProperty Float floatProp;\n"
    -                        + "  @Document.DoubleProperty Double doubleProp;\n"
    -                        + "  @Document.BooleanProperty Boolean booleanProp;\n"
    -                        + "  @Document.BytesProperty byte[] bytesProp;\n"
    -                        //+ "  @Document.Property Gift documentProp;\n"
    +                        + "  @StringProperty String stringProp;\n"
    +                        + "  @LongProperty Integer integerProp;\n"
    +                        + "  @LongProperty Long longProp;\n"
    +                        + "  @DoubleProperty Float floatProp;\n"
    +                        + "  @DoubleProperty Double doubleProp;\n"
    +                        + "  @BooleanProperty Boolean booleanProp;\n"
    +                        + "  @BytesProperty byte[] bytesProp;\n"
    +                        + "  @EmbeddingProperty EmbeddingVector vectorProp;\n"
    +                        //+ "  @DocumentProperty Gift documentProp;\n"
                             + "}\n");
     
             assertThat(compilation).succeededWithoutWarnings();
    @@ -1260,6 +1262,7 @@
             Compilation compilation = compile(
                     "import java.util.*;\n"
                             + "import androidx.appsearch.app.GenericDocument;\n"
    +                        + "import androidx.appsearch.app.EmbeddingVector;\n"
                             + "@Document\n"
                             + "public class Gift {\n"
                             + "  @Namespace String namespace;\n"
    @@ -1274,6 +1277,7 @@
                             + "  @BytesProperty Collection collectByteArr;\n"    // 1a
                             + "  @StringProperty Collection collectString;\n"     // 1b
                             + "  @DocumentProperty Collection collectGift;\n"         // 1c
    +                        + "  @EmbeddingProperty Collection collectVec;\n"   // 1b
                             + "\n"
                             + "  // Arrays\n"
                             + "  @LongProperty Long[] arrBoxLong;\n"         // 2a
    @@ -1289,6 +1293,7 @@
                             + "  @BytesProperty byte[][] arrUnboxByteArr;\n"  // 2b
                             + "  @StringProperty String[] arrString;\n"        // 2b
                             + "  @DocumentProperty Gift[] arrGift;\n"            // 2c
    +                        + "  @EmbeddingProperty EmbeddingVector[] arrVec;\n"         // 2b
                             + "\n"
                             + "  // Single values\n"
                             + "  @StringProperty String string;\n"        // 3a
    @@ -1304,6 +1309,7 @@
                             + "  @BooleanProperty boolean unboxBoolean;\n" // 3b
                             + "  @BytesProperty byte[] unboxByteArr;\n"  // 3a
                             + "  @DocumentProperty Gift gift;\n"            // 3c
    +                        + "  @EmbeddingProperty EmbeddingVector vec;\n"        // 3a
                             + "}\n");
     
             assertThat(compilation).succeededWithoutWarnings();
    @@ -3480,6 +3486,38 @@
             checkEqualsGolden("Gift.java");
         }
     
    +    @Test
    +    public void testEmbeddingFields() throws Exception {
    +        Compilation compilation = compile(
    +                "import java.util.*;\n"
    +                        + "import androidx.appsearch.app.EmbeddingVector;\n"
    +                        + "@Document\n"
    +                        + "public class Gift {\n"
    +                        + "  @Document.Namespace String namespace;\n"
    +                        + "  @Document.Id String id;\n"
    +                        + "  @Document.StringProperty String name;\n"
    +                        // Embedding properties
    +                        + "  @EmbeddingProperty EmbeddingVector defaultIndexNone;\n"
    +                        + "  @EmbeddingProperty(indexingType=0) EmbeddingVector indexNone;\n"
    +                        + "  @EmbeddingProperty(indexingType=1) EmbeddingVector vec;\n"
    +                        + "  @EmbeddingProperty(indexingType=1) List listVec;\n"
    +                        + "  @EmbeddingProperty(indexingType=1)"
    +                        + "  Collection collectVec;\n"
    +                        + "  @EmbeddingProperty(indexingType=1) EmbeddingVector[] arrVec;\n"
    +                        + "}\n");
    +
    +        assertThat(compilation).succeededWithoutWarnings();
    +        checkResultContains("Gift.java",
    +                "new AppSearchSchema.EmbeddingPropertyConfig.Builder");
    +        checkResultContains("Gift.java",
    +                "AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY");
    +        checkResultContains("Gift.java",
    +                "AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE");
    +        checkResultContains("Gift.java",
    +                "EmbeddingVector");
    +        checkEqualsGolden("Gift.java");
    +    }
    +
         private Compilation compile(String classBody) {
             return compile("Gift", classBody, /* restrictGeneratedCodeToLibrary= */false);
         }
    
    diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
    index 4d65cc9..387d97d 100644
    --- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
    +++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAllSingleTypes.JAVA
    
    @@ -2,6 +2,7 @@
     
     import androidx.appsearch.app.AppSearchSchema;
     import androidx.appsearch.app.DocumentClassFactory;
    +import androidx.appsearch.app.EmbeddingVector;
     import androidx.appsearch.app.GenericDocument;
     import androidx.appsearch.exceptions.AppSearchException;
     import java.lang.Boolean;
    @@ -55,6 +56,10 @@
               .addProperty(new AppSearchSchema.BytesPropertyConfig.Builder("bytesProp")
                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                 .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vectorProp")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
    +            .build())
               .build();
       }
     
    @@ -95,6 +100,10 @@
         if (bytesPropCopy != null) {
           builder.setPropertyBytes("bytesProp", bytesPropCopy);
         }
    +    EmbeddingVector vectorPropCopy = document.vectorProp;
    +    if (vectorPropCopy != null) {
    +      builder.setPropertyEmbedding("vectorProp", vectorPropCopy);
    +    }
         return builder.build();
       }
     
    @@ -138,6 +147,11 @@
         if (bytesPropCopy != null && bytesPropCopy.length != 0) {
           bytesPropConv = bytesPropCopy[0];
         }
    +    EmbeddingVector[] vectorPropCopy = genericDoc.getPropertyEmbeddingArray("vectorProp");
    +    EmbeddingVector vectorPropConv = null;
    +    if (vectorPropCopy != null && vectorPropCopy.length != 0) {
    +      vectorPropConv = vectorPropCopy[0];
    +    }
         Gift document = new Gift();
         document.namespace = namespaceConv;
         document.id = idConv;
    @@ -148,6 +162,7 @@
         document.doubleProp = doublePropConv;
         document.booleanProp = booleanPropConv;
         document.bytesProp = bytesPropConv;
    +    document.vectorProp = vectorPropConv;
         return document;
       }
     }
    
    diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
    new file mode 100644
    index 0000000..9447804
    --- /dev/null
    +++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testEmbeddingFields.JAVA
    
    @@ -0,0 +1,153 @@
    +package com.example.appsearch;
    +
    +import androidx.appsearch.app.AppSearchSchema;
    +import androidx.appsearch.app.DocumentClassFactory;
    +import androidx.appsearch.app.EmbeddingVector;
    +import androidx.appsearch.app.GenericDocument;
    +import androidx.appsearch.exceptions.AppSearchException;
    +import java.lang.Class;
    +import java.lang.Override;
    +import java.lang.String;
    +import java.util.Arrays;
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.Map;
    +import javax.annotation.processing.Generated;
    +
    +@Generated("androidx.appsearch.compiler.AppSearchCompiler")
    +public final class $$__AppSearch__Gift implements DocumentClassFactory {
    +  public static final String SCHEMA_NAME = "Gift";
    +
    +  @Override
    +  public String getSchemaName() {
    +    return SCHEMA_NAME;
    +  }
    +
    +  @Override
    +  public AppSearchSchema getSchema() throws AppSearchException {
    +    return new AppSearchSchema.Builder(SCHEMA_NAME)
    +          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("name")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
    +            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
    +            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
    +            .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
    +            .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("defaultIndexNone")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
    +            .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("indexNone")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
    +            .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
    +            .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("listVec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
    +            .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("collectVec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
    +            .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("arrVec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
    +            .build())
    +          .build();
    +  }
    +
    +  @Override
    +  public List> getDependencyDocumentClasses() throws AppSearchException {
    +    return Collections.emptyList();
    +  }
    +
    +  @Override
    +  public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
    +    GenericDocument.Builder builder =
    +        new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
    +    String nameCopy = document.name;
    +    if (nameCopy != null) {
    +      builder.setPropertyString("name", nameCopy);
    +    }
    +    EmbeddingVector defaultIndexNoneCopy = document.defaultIndexNone;
    +    if (defaultIndexNoneCopy != null) {
    +      builder.setPropertyEmbedding("defaultIndexNone", defaultIndexNoneCopy);
    +    }
    +    EmbeddingVector indexNoneCopy = document.indexNone;
    +    if (indexNoneCopy != null) {
    +      builder.setPropertyEmbedding("indexNone", indexNoneCopy);
    +    }
    +    EmbeddingVector vecCopy = document.vec;
    +    if (vecCopy != null) {
    +      builder.setPropertyEmbedding("vec", vecCopy);
    +    }
    +    List listVecCopy = document.listVec;
    +    if (listVecCopy != null) {
    +      EmbeddingVector[] listVecConv = listVecCopy.toArray(new EmbeddingVector[0]);
    +      builder.setPropertyEmbedding("listVec", listVecConv);
    +    }
    +    Collection collectVecCopy = document.collectVec;
    +    if (collectVecCopy != null) {
    +      EmbeddingVector[] collectVecConv = collectVecCopy.toArray(new EmbeddingVector[0]);
    +      builder.setPropertyEmbedding("collectVec", collectVecConv);
    +    }
    +    EmbeddingVector[] arrVecCopy = document.arrVec;
    +    if (arrVecCopy != null) {
    +      builder.setPropertyEmbedding("arrVec", arrVecCopy);
    +    }
    +    return builder.build();
    +  }
    +
    +  @Override
    +  public Gift fromGenericDocument(GenericDocument genericDoc,
    +      Map> documentClassMap) throws AppSearchException {
    +    String namespaceConv = genericDoc.getNamespace();
    +    String idConv = genericDoc.getId();
    +    String[] nameCopy = genericDoc.getPropertyStringArray("name");
    +    String nameConv = null;
    +    if (nameCopy != null && nameCopy.length != 0) {
    +      nameConv = nameCopy[0];
    +    }
    +    EmbeddingVector[] defaultIndexNoneCopy = genericDoc.getPropertyEmbeddingArray("defaultIndexNone");
    +    EmbeddingVector defaultIndexNoneConv = null;
    +    if (defaultIndexNoneCopy != null && defaultIndexNoneCopy.length != 0) {
    +      defaultIndexNoneConv = defaultIndexNoneCopy[0];
    +    }
    +    EmbeddingVector[] indexNoneCopy = genericDoc.getPropertyEmbeddingArray("indexNone");
    +    EmbeddingVector indexNoneConv = null;
    +    if (indexNoneCopy != null && indexNoneCopy.length != 0) {
    +      indexNoneConv = indexNoneCopy[0];
    +    }
    +    EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
    +    EmbeddingVector vecConv = null;
    +    if (vecCopy != null && vecCopy.length != 0) {
    +      vecConv = vecCopy[0];
    +    }
    +    EmbeddingVector[] listVecCopy = genericDoc.getPropertyEmbeddingArray("listVec");
    +    List listVecConv = null;
    +    if (listVecCopy != null) {
    +      listVecConv = Arrays.asList(listVecCopy);
    +    }
    +    EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
    +    List collectVecConv = null;
    +    if (collectVecCopy != null) {
    +      collectVecConv = Arrays.asList(collectVecCopy);
    +    }
    +    EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
    +    Gift document = new Gift();
    +    document.namespace = namespaceConv;
    +    document.id = idConv;
    +    document.name = nameConv;
    +    document.defaultIndexNone = defaultIndexNoneConv;
    +    document.indexNone = indexNoneConv;
    +    document.vec = vecConv;
    +    document.listVec = listVecConv;
    +    document.collectVec = collectVecConv;
    +    document.arrVec = arrVecConv;
    +    return document;
    +  }
    +}
    
    diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
    index ee36e9d6..6ac5c27 100644
    --- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
    +++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testToGenericDocument_allSupportedTypes.JAVA
    
    @@ -2,6 +2,7 @@
     
     import androidx.appsearch.app.AppSearchSchema;
     import androidx.appsearch.app.DocumentClassFactory;
    +import androidx.appsearch.app.EmbeddingVector;
     import androidx.appsearch.app.GenericDocument;
     import androidx.appsearch.exceptions.AppSearchException;
     import java.lang.Boolean;
    @@ -61,6 +62,10 @@
                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                 .setShouldIndexNestedProperties(false)
                 .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("collectVec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
    +            .build())
               .addProperty(new AppSearchSchema.LongPropertyConfig.Builder("arrBoxLong")
                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                 .setIndexingType(AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE)
    @@ -108,6 +113,10 @@
                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                 .setShouldIndexNestedProperties(false)
                 .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("arrVec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
    +            .build())
               .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("string")
                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                 .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
    @@ -155,6 +164,10 @@
                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
                 .setShouldIndexNestedProperties(false)
                 .build())
    +          .addProperty(new AppSearchSchema.EmbeddingPropertyConfig.Builder("vec")
    +            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
    +            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_NONE)
    +            .build())
               .build();
       }
     
    @@ -237,6 +250,11 @@
           }
           builder.setPropertyDocument("collectGift", collectGiftConv);
         }
    +    Collection collectVecCopy = document.collectVec;
    +    if (collectVecCopy != null) {
    +      EmbeddingVector[] collectVecConv = collectVecCopy.toArray(new EmbeddingVector[0]);
    +      builder.setPropertyEmbedding("collectVec", collectVecConv);
    +    }
         Long[] arrBoxLongCopy = document.arrBoxLong;
         if (arrBoxLongCopy != null) {
           long[] arrBoxLongConv = new long[arrBoxLongCopy.length];
    @@ -321,6 +339,10 @@
           }
           builder.setPropertyDocument("arrGift", arrGiftConv);
         }
    +    EmbeddingVector[] arrVecCopy = document.arrVec;
    +    if (arrVecCopy != null) {
    +      builder.setPropertyEmbedding("arrVec", arrVecCopy);
    +    }
         String stringCopy = document.string;
         if (stringCopy != null) {
           builder.setPropertyString("string", stringCopy);
    @@ -359,6 +381,10 @@
           GenericDocument giftConv = GenericDocument.fromDocumentClass(giftCopy);
           builder.setPropertyDocument("gift", giftConv);
         }
    +    EmbeddingVector vecCopy = document.vec;
    +    if (vecCopy != null) {
    +      builder.setPropertyEmbedding("vec", vecCopy);
    +    }
         return builder.build();
       }
     
    @@ -428,6 +454,11 @@
             collectGiftConv.add(collectGiftCopy[i].toDocumentClass(Gift.class, documentClassMap));
           }
         }
    +    EmbeddingVector[] collectVecCopy = genericDoc.getPropertyEmbeddingArray("collectVec");
    +    List collectVecConv = null;
    +    if (collectVecCopy != null) {
    +      collectVecConv = Arrays.asList(collectVecCopy);
    +    }
         long[] arrBoxLongCopy = genericDoc.getPropertyLongArray("arrBoxLong");
         Long[] arrBoxLongConv = null;
         if (arrBoxLongCopy != null) {
    @@ -497,6 +528,7 @@
             arrGiftConv[i] = arrGiftCopy[i].toDocumentClass(Gift.class, documentClassMap);
           }
         }
    +    EmbeddingVector[] arrVecConv = genericDoc.getPropertyEmbeddingArray("arrVec");
         String[] stringCopy = genericDoc.getPropertyStringArray("string");
         String stringConv = null;
         if (stringCopy != null && stringCopy.length != 0) {
    @@ -542,6 +574,11 @@
         if (giftCopy != null) {
           giftConv = giftCopy.toDocumentClass(Gift.class, documentClassMap);
         }
    +    EmbeddingVector[] vecCopy = genericDoc.getPropertyEmbeddingArray("vec");
    +    EmbeddingVector vecConv = null;
    +    if (vecCopy != null && vecCopy.length != 0) {
    +      vecConv = vecCopy[0];
    +    }
         Gift document = new Gift();
         document.namespace = namespaceConv;
         document.id = idConv;
    @@ -553,6 +590,7 @@
         document.collectByteArr = collectByteArrConv;
         document.collectString = collectStringConv;
         document.collectGift = collectGiftConv;
    +    document.collectVec = collectVecConv;
         document.arrBoxLong = arrBoxLongConv;
         document.arrUnboxLong = arrUnboxLongConv;
         document.arrBoxInteger = arrBoxIntegerConv;
    @@ -566,6 +604,7 @@
         document.arrUnboxByteArr = arrUnboxByteArrConv;
         document.arrString = arrStringConv;
         document.arrGift = arrGiftConv;
    +    document.arrVec = arrVecConv;
         document.string = stringConv;
         document.boxLong = boxLongConv;
         document.unboxLong = unboxLongConv;
    @@ -579,6 +618,7 @@
         document.unboxBoolean = unboxBooleanConv;
         document.unboxByteArr = unboxByteArrConv;
         document.gift = giftConv;
    +    document.vec = vecConv;
         return document;
       }
     }
    
    diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
    index 8b3a414..b483b72 100755
    --- a/appsearch/exportToFramework.py
    +++ b/appsearch/exportToFramework.py
    
    @@ -64,7 +64,8 @@
     FRAMEWORK_API_TEST_ROOT = 'testing/coretests/src/android/app/appsearch/external'
     FRAMEWORK_IMPL_ROOT = 'service/java/com/android/server/appsearch/external'
     FRAMEWORK_IMPL_TEST_ROOT = 'testing/servicestests/src/com/android/server/appsearch/external'
    -FRAMEWORK_TEST_UTIL_ROOT = '../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external'
    +FRAMEWORK_TEST_UTIL_ROOT = (
    +    '../../../cts/tests/appsearch/testutils/src/android/app/appsearch/testutil/external')
     FRAMEWORK_TEST_UTIL_TEST_ROOT = 'testing/servicestests/src/android/app/appsearch/testutil/external'
     FRAMEWORK_CTS_TEST_ROOT = '../../../cts/tests/appsearch/src/com/android/cts/appsearch/external'
     GOOGLE_JAVA_FORMAT = (
    @@ -88,19 +89,34 @@
                     os.remove(abs_path)
     
         def _TransformAndCopyFile(
    -            self, source_path, dest_path, transform_func=None, ignore_skips=False):
    +            self, source_path, default_dest_path, transform_func=None, ignore_skips=False):
    +        """
    +        Transforms the file located at 'source_path' and writes it into 'default_dest_path'.
    +
    +        An @exportToFramework:skip() directive will skip the copy process.
    +        An @exportToFramework:copyToPath() directive will override default_dest_path with another
    +          path relative to framework_appsearch_root (which is usually packages/modules/AppSearch)
    +        """
             with open(source_path, 'r') as fh:
                 contents = fh.read()
     
             if not ignore_skips and '@exportToFramework:skipFile()' in contents:
    -            print('Skipping: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
    +            print('Skipping: "%s" -> "%s"' % (source_path, default_dest_path), file=sys.stderr)
                 return
     
    -        copyToPath = re.search(r'@exportToFramework:copyToPath\(([^)]+)\)', contents)
    -        if copyToPath:
    -          dest_path = os.path.join(self._framework_appsearch_root, copyToPath.group(1))
    +        copy_to_path = re.search(r'@exportToFramework:copyToPath\(([^)]+)\)', contents)
    +        if copy_to_path:
    +            dest_path = os.path.join(self._framework_appsearch_root, copy_to_path.group(1))
    +        else:
    +            dest_path = default_dest_path
     
    +        self._TransformAndCopyFileToPath(source_path, dest_path, transform_func)
    +
    +    def _TransformAndCopyFileToPath(self, source_path, dest_path, transform_func=None):
    +        """Transforms the file located at 'source_path' and writes it into 'dest_path'."""
             print('Copy: "%s" -> "%s"' % (source_path, dest_path), file=sys.stderr)
    +        with open(source_path, 'r') as fh:
    +            contents = fh.read()
             if transform_func:
                 contents = transform_func(contents)
             os.makedirs(os.path.dirname(dest_path), exist_ok=True)
    @@ -149,6 +165,8 @@
                 .replace(
                         'androidx.appsearch.localstorage.',
                         'com.android.server.appsearch.external.localstorage.')
    +            .replace('androidx.appsearch.flags.FlaggedApi', 'android.annotation.FlaggedApi')
    +            .replace('androidx.appsearch.flags.Flags', 'com.android.appsearch.flags.Flags')
                 .replace('androidx.appsearch', 'android.app.appsearch')
                 .replace(
                         'androidx.annotation.GuardedBy',
    @@ -180,6 +198,7 @@
                 .replace('// @exportToFramework:skipFile()', '')
             )
             contents = re.sub(r'\/\/ @exportToFramework:copyToPath\([^)]+\)', '', contents)
    +        contents = re.sub(r'@RequiresFeature\([^)]*\)', '', contents, flags=re.DOTALL)
     
             # Jetpack methods have the Async suffix, but framework doesn't. Strip the Async suffix
             # to allow the same documentation to compile for both.
    @@ -190,16 +209,27 @@
     
         def _TransformTestCode(self, contents):
             contents = (contents
    +            .replace(
    +                    'androidx.appsearch.flags.CheckFlagsRule',
    +                    'android.platform.test.flag.junit.CheckFlagsRule')
    +            .replace(
    +                    'androidx.appsearch.flags.DeviceFlagsValueProvider',
    +                    'android.platform.test.flag.junit.DeviceFlagsValueProvider')
    +            .replace(
    +                    'androidx.appsearch.flags.RequiresFlagsEnabled',
    +                    'android.platform.test.annotations.RequiresFlagsEnabled')
                 .replace('androidx.appsearch.testutil.', 'android.app.appsearch.testutil.')
                 .replace(
                         'package androidx.appsearch.testutil;',
                         'package android.app.appsearch.testutil;')
                 .replace(
    -                    'androidx.appsearch.localstorage.LocalStorage',
    -                    'android.app.appsearch.AppSearchManager')
    +                    'import androidx.appsearch.localstorage.LocalStorage;',
    +                    'import android.app.appsearch.AppSearchManager;')
                 .replace('LocalStorage.', 'AppSearchManager.')
             )
    -        for shim in ['AppSearchSession', 'GlobalSearchSession', 'SearchResults']:
    +        for shim in [
    +                'AppSearchSession', 'GlobalSearchSession', 'EnterpriseGlobalSearchSession',
    +                'SearchResults']:
                 contents = re.sub(r"([^a-zA-Z])(%s)([^a-zA-Z0-9])" % shim, r'\1\2Shim\3', contents)
             return self._TransformCommonCode(contents)
     
    @@ -277,7 +307,8 @@
             self._TransformAndCopyFolder(
                     test_util_source_dir, test_util_dest_dir, transform_func=self._TransformTestCode)
             for iface_file in (
    -                'AppSearchSession.java', 'GlobalSearchSession.java', 'SearchResults.java'):
    +                'AppSearchSession.java', 'GlobalSearchSession.java',
    +                'EnterpriseGlobalSearchSession.java', 'SearchResults.java'):
                 dest_file_name = os.path.splitext(iface_file)[0] + 'Shim.java'
                 self._TransformAndCopyFile(
                         os.path.join(api_source_dir, 'app/' + iface_file),
    @@ -318,6 +349,8 @@
                                 'package com.android.server.appsearch.external')
                         .replace('com.google.android.icing.proto.',
                                 'com.android.server.appsearch.icing.proto.')
    +                    .replace('com.google.android.appsearch.proto.',
    +                            'com.android.server.appsearch.appsearch.proto.')
                         .replace('com.google.android.icing.protobuf.',
                                 'com.android.server.appsearch.protobuf.')
                 )
    @@ -360,6 +393,32 @@
             print('Wrote "%s"' % file_path)
             return old_sha
     
    +    def FormatCommitMessage(self, old_sha, new_sha):
    +        print('\nCommand to diff old version to new version:')
    +        print('  git log --pretty=format:"* %h %s" {}..{} -- appsearch/'.format(old_sha, new_sha))
    +        pretty_log = subprocess.check_output([
    +            'git',
    +            'log',
    +            '--pretty=format:* %h %s',
    +            '{}..{}'.format(old_sha, new_sha),
    +            '--',
    +            'appsearch/'
    +        ]).decode("utf-8")
    +        bug_output = subprocess.check_output([
    +            '/bin/sh',
    +            '-c',
    +            'git log {}..{} -- appsearch/ | grep Bug: | sort | uniq'.format(old_sha, new_sha)
    +        ]).decode("utf-8")
    +
    +        print('\n--------------------------------------------------')
    +        print('Update Framework from Jetpack.\n')
    +        print(pretty_log)
    +        print()
    +        for line in bug_output.splitlines():
    +            print(line.strip())
    +        print('Test: Presubmit\n')
    +        print('--------------------------------------------------\n')
    +
     
     if __name__ == '__main__':
         if len(sys.argv) != 3:
    @@ -388,5 +447,4 @@
         new_sha = sys.argv[2]
         old_sha = exporter.WriteShaFile(new_sha)
         if old_sha and old_sha != new_sha:
    -      print('Command to diff old version to new version:')
    -      print('  git log %s..%s -- appsearch/' % (old_sha, new_sha))
    +        exporter.FormatCommitMessage(old_sha, new_sha)
    
    diff --git a/biometric/biometric/api/current.txt b/biometric/biometric/api/current.txt
    index 6a60ed1..eb18aba 100644
    --- a/biometric/biometric/api/current.txt
    +++ b/biometric/biometric/api/current.txt
    
    @@ -43,6 +43,7 @@
         field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
         field public static final int ERROR_LOCKOUT = 7; // 0x7
         field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
    +    field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10
         field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
         field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
         field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
    @@ -72,16 +73,22 @@
         ctor public BiometricPrompt.CryptoObject(java.security.Signature);
         ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
         ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
    +    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long);
         method public javax.crypto.Cipher? getCipher();
         method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
         method public javax.crypto.Mac? getMac();
    +    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle();
         method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession();
         method public java.security.Signature? getSignature();
       }
     
       public static class BiometricPrompt.PromptInfo {
         method public int getAllowedAuthenticators();
    +    method public androidx.biometric.PromptContentView? getContentView();
         method public CharSequence? getDescription();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();
    +    method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
         method public CharSequence getNegativeButtonText();
         method public CharSequence? getSubtitle();
         method public CharSequence getTitle();
    @@ -94,13 +101,54 @@
         method public androidx.biometric.BiometricPrompt.PromptInfo build();
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
    +    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
         method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
       }
     
    +  public interface PromptContentItem {
    +  }
    +
    +  public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {
    +    ctor public PromptContentItemBulletedText(String);
    +  }
    +
    +  public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {
    +    ctor public PromptContentItemPlainText(String);
    +  }
    +
    +  public interface PromptContentView {
    +  }
    +
    +  public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();
    +  }
    +
    +  public static final class PromptContentViewWithMoreOptionsButton.Builder {
    +    ctor public PromptContentViewWithMoreOptionsButton.Builder();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);
    +  }
    +
    +  public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {
    +    method public String? getDescription();
    +    method public java.util.List getListItems();
    +  }
    +
    +  public static final class PromptVerticalListContentView.Builder {
    +    ctor public PromptVerticalListContentView.Builder();
    +    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);
    +    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);
    +    method public androidx.biometric.PromptVerticalListContentView build();
    +    method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);
    +  }
    +
     }
     
     package androidx.biometric.auth {
    
    diff --git a/biometric/biometric/api/restricted_current.txt b/biometric/biometric/api/restricted_current.txt
    index 6a60ed1..eb18aba 100644
    --- a/biometric/biometric/api/restricted_current.txt
    +++ b/biometric/biometric/api/restricted_current.txt
    
    @@ -43,6 +43,7 @@
         field public static final int ERROR_HW_UNAVAILABLE = 1; // 0x1
         field public static final int ERROR_LOCKOUT = 7; // 0x7
         field public static final int ERROR_LOCKOUT_PERMANENT = 9; // 0x9
    +    field public static final int ERROR_MORE_OPTIONS_BUTTON = 16; // 0x10
         field public static final int ERROR_NEGATIVE_BUTTON = 13; // 0xd
         field public static final int ERROR_NO_BIOMETRICS = 11; // 0xb
         field public static final int ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
    @@ -72,16 +73,22 @@
         ctor public BiometricPrompt.CryptoObject(java.security.Signature);
         ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher);
         ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac);
    +    ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long);
         method public javax.crypto.Cipher? getCipher();
         method @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential();
         method public javax.crypto.Mac? getMac();
    +    method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle();
         method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession();
         method public java.security.Signature? getSignature();
       }
     
       public static class BiometricPrompt.PromptInfo {
         method public int getAllowedAuthenticators();
    +    method public androidx.biometric.PromptContentView? getContentView();
         method public CharSequence? getDescription();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap? getLogoBitmap();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getLogoDescription();
    +    method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
         method public CharSequence getNegativeButtonText();
         method public CharSequence? getSubtitle();
         method public CharSequence getTitle();
    @@ -94,13 +101,54 @@
         method public androidx.biometric.BiometricPrompt.PromptInfo build();
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setAllowedAuthenticators(int);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setConfirmationRequired(boolean);
    +    method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setContentView(androidx.biometric.PromptContentView);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDescription(CharSequence?);
         method @Deprecated public androidx.biometric.BiometricPrompt.PromptInfo.Builder setDeviceCredentialAllowed(boolean);
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoBitmap(android.graphics.Bitmap);
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoDescription(String);
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.BiometricPrompt.PromptInfo.Builder setLogoRes(@DrawableRes int);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setNegativeButtonText(CharSequence);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setSubtitle(CharSequence?);
         method public androidx.biometric.BiometricPrompt.PromptInfo.Builder setTitle(CharSequence);
       }
     
    +  public interface PromptContentItem {
    +  }
    +
    +  public final class PromptContentItemBulletedText implements androidx.biometric.PromptContentItem {
    +    ctor public PromptContentItemBulletedText(String);
    +  }
    +
    +  public final class PromptContentItemPlainText implements androidx.biometric.PromptContentItem {
    +    ctor public PromptContentItemPlainText(String);
    +  }
    +
    +  public interface PromptContentView {
    +  }
    +
    +  public final class PromptContentViewWithMoreOptionsButton implements androidx.biometric.PromptContentView {
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String? getDescription();
    +  }
    +
    +  public static final class PromptContentViewWithMoreOptionsButton.Builder {
    +    ctor public PromptContentViewWithMoreOptionsButton.Builder();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton build();
    +    method @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public androidx.biometric.PromptContentViewWithMoreOptionsButton.Builder setDescription(String);
    +  }
    +
    +  public final class PromptVerticalListContentView implements androidx.biometric.PromptContentView {
    +    method public String? getDescription();
    +    method public java.util.List getListItems();
    +  }
    +
    +  public static final class PromptVerticalListContentView.Builder {
    +    ctor public PromptVerticalListContentView.Builder();
    +    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem);
    +    method public androidx.biometric.PromptVerticalListContentView.Builder addListItem(androidx.biometric.PromptContentItem, int);
    +    method public androidx.biometric.PromptVerticalListContentView build();
    +    method public androidx.biometric.PromptVerticalListContentView.Builder setDescription(String);
    +  }
    +
     }
     
     package androidx.biometric.auth {
    
    diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
    index 1a3dfdc..f07feda8 100644
    --- a/biometric/biometric/build.gradle
    +++ b/biometric/biometric/build.gradle
    
    @@ -32,6 +32,7 @@
         defaultConfig {
             multiDexEnabled true
         }
    +    compileSdkPreview "VanillaIceCream"
     }
     
     dependencies {
    @@ -76,6 +77,7 @@
         }
         testOptions.unitTests.includeAndroidResources = true
         namespace "androidx.biometric"
    +    compileSdkPreview "VanillaIceCream"
     }
     
     androidx {
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
    index c29b0bb..36b6817 100644
    --- a/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
    +++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricFragment.java
    
    @@ -16,11 +16,13 @@
     
     package androidx.biometric;
     
    +import android.annotation.SuppressLint;
     import android.app.Activity;
     import android.app.KeyguardManager;
     import android.content.Context;
     import android.content.DialogInterface;
     import android.content.Intent;
    +import android.graphics.Bitmap;
     import android.os.Build;
     import android.os.Bundle;
     import android.os.Handler;
    @@ -81,13 +83,20 @@
         static final int CANCELED_FROM_CLIENT = 3;
     
         /**
    +     * Authentication was canceled by the user by pressing the more options button on the prompt
    +     * content.
    +     */
    +    static final int CANCELED_FROM_MORE_OPTIONS_BUTTON = 4;
    +
    +    /**
          * Where authentication was canceled from.
          */
         @IntDef({
             CANCELED_FROM_INTERNAL,
             CANCELED_FROM_USER,
             CANCELED_FROM_NEGATIVE_BUTTON,
    -        CANCELED_FROM_CLIENT
    +        CANCELED_FROM_CLIENT,
    +        CANCELED_FROM_MORE_OPTIONS_BUTTON
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface CanceledFrom {}
    @@ -352,6 +361,15 @@
                         }
                     });
     
    +        mViewModel.isMoreOptionsButtonPressPending().observe(this,
    +                moreOptionsButtonPressPending -> {
    +                    if (moreOptionsButtonPressPending) {
    +                        onMoreOptionsButtonPressed();
    +                        mViewModel.setMoreOptionsButtonPressPending(false);
    +                    }
    +                }
    +        );
    +
             mViewModel.isFingerprintDialogCancelPending().observe(this,
                     fingerprintDialogCancelPending -> {
                         if (fingerprintDialogCancelPending) {
    @@ -514,6 +532,29 @@
                         builder, AuthenticatorUtils.isDeviceCredentialAllowed(authenticators));
             }
     
    +        // Set the custom biometric prompt features introduced in Android 15 (API 35).
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    +            final int logoRes = mViewModel.getLogoRes();
    +            final Bitmap logoBitmap = mViewModel.getLogoBitmap();
    +            final String logoDescription = mViewModel.getLogoDescription();
    +            final android.hardware.biometrics.PromptContentView contentView =
    +                    PromptContentViewUtils.wrapForBiometricPrompt(mViewModel.getContentView(),
    +                            mViewModel.getClientExecutor(),
    +                            mViewModel.getMoreOptionsButtonListener());
    +            if (logoRes != -1) {
    +                Api35Impl.setLogoRes(builder, logoRes);
    +            }
    +            if (logoBitmap != null) {
    +                Api35Impl.setLogoBitmap(builder, logoBitmap);
    +            }
    +            if (logoDescription != null && !logoDescription.isEmpty()) {
    +                Api35Impl.setLogoDescription(builder, logoDescription);
    +            }
    +            if (contentView != null) {
    +                Api35Impl.setContentView(builder, contentView);
    +            }
    +        }
    +
             authenticateWithBiometricPrompt(Api28Impl.buildPrompt(builder), getContext());
         }
     
    @@ -779,6 +820,16 @@
         }
     
         /**
    +     * Callback that is run when the view model reports that the more options button has been
    +     * pressed on the prompt content.
    +     */
    +    void onMoreOptionsButtonPressed() {
    +        sendErrorAndDismiss(BiometricPrompt.ERROR_MORE_OPTIONS_BUTTON,
    +                "More options button in the content view is clicked.");
    +        cancelAuthentication(BiometricFragment.CANCELED_FROM_MORE_OPTIONS_BUTTON);
    +    }
    +
    +    /**
          * Launches the confirm device credential Settings activity, where the user can authenticate
          * using their PIN, pattern, or password.
          */
    @@ -1097,6 +1148,72 @@
         }
     
         /**
    +     * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
    +     */
    +    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    +    @SuppressLint("MissingPermission")
    +    private static class Api35Impl {
    +        // Prevent instantiation.
    +        private Api35Impl() {
    +        }
    +
    +        /**
    +         * Sets the prompt content view for the given framework prompt builder.
    +         *
    +         * @param builder An instance of
    +         *                {@link android.hardware.biometrics.BiometricPrompt.Builder}.
    +         * @param logoRes A drawable resource of the logo that will be shown on the prompt.
    +         */
    +        @DoNotInline
    +        static void setLogoRes(
    +                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder, int logoRes) {
    +            builder.setLogoRes(logoRes);
    +        }
    +
    +        /**
    +         * Sets the prompt content view for the given framework prompt builder.
    +         *
    +         * @param builder    An instance of
    +         *                   {@link android.hardware.biometrics.BiometricPrompt.Builder}.
    +         * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt.
    +         */
    +        @DoNotInline
    +        static void setLogoBitmap(
    +                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
    +                @NonNull Bitmap logoBitmap) {
    +            builder.setLogoBitmap(logoBitmap);
    +        }
    +
    +        /**
    +         * Sets the prompt content view for the given framework prompt builder.
    +         *
    +         * @param builder         An instance of
    +         *                        {@link android.hardware.biometrics.BiometricPrompt.Builder}.
    +         * @param logoDescription The content view for the prompt.
    +         */
    +        @DoNotInline
    +        static void setLogoDescription(
    +                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
    +                String logoDescription) {
    +            builder.setLogoDescription(logoDescription);
    +        }
    +
    +        /**
    +         * Sets the prompt content view for the given framework prompt builder.
    +         *
    +         * @param builder     An instance of
    +         *                    {@link android.hardware.biometrics.BiometricPrompt.Builder}.
    +         * @param contentView The content view for the prompt.
    +         */
    +        @DoNotInline
    +        static void setContentView(
    +                @NonNull android.hardware.biometrics.BiometricPrompt.Builder builder,
    +                @NonNull android.hardware.biometrics.PromptContentView contentView) {
    +            builder.setContentView(contentView);
    +        }
    +    }
    +
    +    /**
          * Nested class to avoid verification errors for methods introduced in Android 11 (API 30).
          */
         @RequiresApi(Build.VERSION_CODES.R)
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
    index ed361eb..01ff4b7 100644
    --- a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
    +++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
    
    @@ -16,15 +16,20 @@
     
     package androidx.biometric;
     
    +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
    +
     import android.annotation.SuppressLint;
    +import android.graphics.Bitmap;
     import android.os.Build;
     import android.text.TextUtils;
     import android.util.Log;
     
    +import androidx.annotation.DrawableRes;
     import androidx.annotation.IntDef;
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RequiresApi;
    +import androidx.annotation.RequiresPermission;
     import androidx.annotation.RestrictTo;
     import androidx.annotation.VisibleForTesting;
     import androidx.biometric.BiometricManager.Authenticators;
    @@ -160,6 +165,11 @@
         public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15;
     
         /**
    +     * The user pressed the more options button on prompt content.
    +     */
    +    public static final int ERROR_MORE_OPTIONS_BUTTON = 16;
    +
    +    /**
          * An error code that may be returned during authentication.
          */
         @IntDef({
    @@ -175,7 +185,8 @@
             ERROR_NO_BIOMETRICS,
             ERROR_HW_NOT_PRESENT,
             ERROR_NEGATIVE_BUTTON,
    -        ERROR_NO_DEVICE_CREDENTIAL
    +        ERROR_NO_DEVICE_CREDENTIAL,
    +        ERROR_MORE_OPTIONS_BUTTON
         })
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         @Retention(RetentionPolicy.SOURCE)
    @@ -230,6 +241,7 @@
             @Nullable private final Mac mMac;
             @Nullable private final android.security.identity.IdentityCredential mIdentityCredential;
             @Nullable private final android.security.identity.PresentationSession mPresentationSession;
    +        private final long mOperationHandle;
     
             /**
              * Creates a crypto object that wraps the given signature object.
    @@ -242,6 +254,7 @@
                 mMac = null;
                 mIdentityCredential = null;
                 mPresentationSession = null;
    +            mOperationHandle = 0;
             }
     
             /**
    @@ -255,6 +268,7 @@
                 mMac = null;
                 mIdentityCredential = null;
                 mPresentationSession = null;
    +            mOperationHandle = 0;
             }
     
             /**
    @@ -268,6 +282,7 @@
                 mMac = mac;
                 mIdentityCredential = null;
                 mPresentationSession = null;
    +            mOperationHandle = 0;
             }
     
             /**
    @@ -284,6 +299,7 @@
                 mMac = null;
                 mIdentityCredential = identityCredential;
                 mPresentationSession = null;
    +            mOperationHandle = 0;
             }
     
             /**
    @@ -300,9 +316,27 @@
                 mMac = null;
                 mIdentityCredential = null;
                 mPresentationSession = presentationSession;
    +            mOperationHandle = 0;
             }
     
             /**
    +         * Create from an operation handle.
    +         * @see CryptoObject#getOperationHandle()
    +         *
    +         * @param operationHandle the operation handle associated with this object.
    +         */
    +        @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    +        public CryptoObject(long operationHandle) {
    +            mSignature = null;
    +            mCipher = null;
    +            mMac = null;
    +            mIdentityCredential = null;
    +            mPresentationSession = null;
    +            mOperationHandle = operationHandle;
    +        }
    +
    +
    +        /**
              * Gets the signature object associated with this crypto object.
              *
              * @return The signature, or {@code null} if none is associated with this object.
    @@ -353,6 +387,34 @@
             public android.security.identity.PresentationSession getPresentationSession() {
                 return mPresentationSession;
             }
    +
    +        /**
    +         * Returns the {@code operationHandle} associated with this object or 0 if none.
    +         * The {@code operationHandle} is the underlying identifier associated with
    +         * the {@code CryptoObject}.
    +         *
    +         * 

    The {@code operationHandle} can be used to reconstruct a {@code CryptoObject} + * instance. This is useful for any cross-process communication as the {@code CryptoObject} + * class is not {@link android.os.Parcelable}. Hence, if the {@code CryptoObject} is + * constructed in one process, and needs to be propagated to another process, + * before calling the {@code authenticate()} API in the second process, the + * recommendation is to retrieve the {@code operationHandle} using this API, and then + * reconstruct the {@code CryptoObject}using the constructor that takes in an {@code + * operationHandle}, and pass that in to the {@code authenticate} API mentioned above. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + public long getOperationHandle() { + return CryptoObjectUtils.getOperationHandle(this); + } + + /** + * Returns the {@code operationHandle} from the constructor. This is only for wrapping + * this {@link androidx.biometric.BiometricPrompt.CryptoObject} to + * {@link android.hardware.biometrics.BiometricPrompt}. + */ + long getOperationHandleCryptoObject() { + return mOperationHandle; + } } /** @@ -438,15 +500,75 @@ */ public static class Builder { // Mutable options to be set on the builder. + @DrawableRes private int mLogoRes = -1; + @Nullable private Bitmap mLogoBitmap = null; + @Nullable private String mLogoDescription = null; @Nullable private CharSequence mTitle = null; @Nullable private CharSequence mSubtitle = null; @Nullable private CharSequence mDescription = null; + @Nullable private PromptContentView mPromptContentView = null; @Nullable private CharSequence mNegativeButtonText = null; private boolean mIsConfirmationRequired = true; private boolean mIsDeviceCredentialAllowed = false; @BiometricManager.AuthenticatorTypes private int mAllowedAuthenticators = 0; /** + * Optional: Sets the drawable resource of the logo that will be shown on the prompt. + * + *

    Note that using this method is not recommended in most scenarios because the + * calling application's icon will be used by default. Setting the logo is intended + * for large bundled applications that perform a wide range of functions and need to + * show distinct icons for each function. + * + * @param logoRes A drawable resource of the logo that will be shown on the prompt. + * @return This builder. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @NonNull + public Builder setLogoRes(@DrawableRes int logoRes) { + mLogoRes = logoRes; + return this; + } + + /** + * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt. + * + *

    Note that using this method is not recommended in most scenarios because the + * calling application's icon will be used by default. Setting the logo is intended + * for large bundled applications that perform a wide range of functions and need to + * show distinct icons for each function. + * + * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt. + * @return This builder. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @NonNull + public Builder setLogoBitmap(@NonNull Bitmap logoBitmap) { + mLogoBitmap = logoBitmap; + return this; + } + + /** + * Optional: Sets logo description text that will be shown on the prompt. + * + *

    Note that using this method is not recommended in most scenarios because the + * calling application's name will be used by default. Setting the logo description + * is intended for large bundled applications that perform a wide range of functions + * and need to show distinct description for each function. + * + * @param logoDescription The logo description text that will be shown on the prompt. + * @return This builder. + * @throws IllegalArgumentException If logo description is null or exceeds certain + * character limit. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @NonNull + public Builder setLogoDescription(@NonNull String logoDescription) { + mLogoDescription = logoDescription; + return this; + } + + /** * Required: Sets the title for the prompt. * * @param title The title to be displayed on the prompt. @@ -471,9 +593,14 @@ } /** - * Optional: Sets the description for the prompt. + * Optional: Sets a description that will be shown on the prompt. * - * @param description The description to be displayed on the prompt. + *

    Note that the description set by {@link Builder#setDescription(CharSequence)} + * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view + * provided to {@link Builder#setContentView(PromptContentView)} will be used if both + * methods are called. + * + * @param description The description to display. * @return This builder. */ @NonNull @@ -483,6 +610,23 @@ } /** + * Optional: Sets application customized content view that will be shown on the prompt. + * + *

    Note that the description set by {@link Builder#setDescription(CharSequence)} + * will be overridden by {@link Builder#setContentView(PromptContentView)}. The view + * provided to {@link Builder#setContentView(PromptContentView)} will be used if both + * methods are called. + * + * @param view The customized view information. + * @return This builder. + */ + @NonNull + public Builder setContentView(@NonNull PromptContentView view) { + mPromptContentView = view; + return this; + } + + /** * Required: Sets the text for the negative button on the prompt. * *

    Note that this option is incompatible with device credential authentication and @@ -623,9 +767,13 @@ } return new PromptInfo( + mLogoRes, + mLogoBitmap, + mLogoDescription, mTitle, mSubtitle, mDescription, + mPromptContentView, mNegativeButtonText, mIsConfirmationRequired, mIsDeviceCredentialAllowed, @@ -634,9 +782,13 @@ } // Immutable fields for the prompt info object. + @DrawableRes private int mLogoRes; + @Nullable private Bitmap mLogoBitmap; + @Nullable private String mLogoDescription; @NonNull private final CharSequence mTitle; @Nullable private final CharSequence mSubtitle; @Nullable private final CharSequence mDescription; + @Nullable private PromptContentView mPromptContentView; @Nullable private final CharSequence mNegativeButtonText; private final boolean mIsConfirmationRequired; private final boolean mIsDeviceCredentialAllowed; @@ -645,16 +797,24 @@ // Prevent direct instantiation. @SuppressWarnings("WeakerAccess") /* synthetic access */ PromptInfo( + int logoRes, + @Nullable Bitmap logoBitmap, + @Nullable String logoDescription, @NonNull CharSequence title, @Nullable CharSequence subtitle, @Nullable CharSequence description, + @Nullable PromptContentView promptContentView, @Nullable CharSequence negativeButtonText, boolean confirmationRequired, boolean deviceCredentialAllowed, @BiometricManager.AuthenticatorTypes int allowedAuthenticators) { + mLogoRes = logoRes; + mLogoBitmap = logoBitmap; + mLogoDescription = logoDescription; mTitle = title; mSubtitle = subtitle; mDescription = description; + mPromptContentView = promptContentView; mNegativeButtonText = negativeButtonText; mIsConfirmationRequired = confirmationRequired; mIsDeviceCredentialAllowed = deviceCredentialAllowed; @@ -662,6 +822,44 @@ } /** + * Gets the drawable resource of the logo for the prompt, as set by + * {@link Builder#setLogoRes(int)}. Currently for system applications use only. + * + * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @DrawableRes + public int getLogoRes() { + return mLogoRes; + } + + /** + * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogoBitmap(Bitmap)}. + * Currently for system applications use only. + * + * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @Nullable + public Bitmap getLogoBitmap() { + return mLogoBitmap; + } + + /** + * Gets the logo description for the prompt, as set by + * {@link Builder#setLogoDescription(String)}. + * Currently for system applications use only. + * + * @return The logo description of the prompt, or null if the prompt has no logo description + * set. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @Nullable + public String getLogoDescription() { + return mLogoDescription; + } + + /** * Gets the title for the prompt. * * @return The title to be displayed on the prompt. @@ -698,6 +896,17 @@ } /** + * Gets the content view for the prompt, as set by + * {@link Builder#setContentView(PromptContentView)}. + * + * @return The content view for the prompt, or null if the prompt has no content view. + */ + @Nullable + public PromptContentView getContentView() { + return mPromptContentView; + } + + /** * Gets the text for the negative button on the prompt. * * @return The label to be used for the negative button on the prompt, or an empty string if

    diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
    index 56f8782..eb6970f 100644
    --- a/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
    +++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricViewModel.java
    
    @@ -16,7 +16,9 @@
     
     package androidx.biometric;
     
    +import android.annotation.SuppressLint;
     import android.content.DialogInterface;
    +import android.graphics.Bitmap;
     import android.os.Handler;
     import android.os.Looper;
     
    @@ -139,6 +141,29 @@
         }
     
         /**
    +     * The dialog listener that is returned by {@link #getMoreOptionsButtonListener()} ()}.
    +     */
    +    private static class MoreOptionsButtonListener implements DialogInterface.OnClickListener {
    +        @NonNull private final WeakReference mViewModelRef;
    +
    +        /**
    +         * Creates a more options button listener with a weak reference to the given view model.
    +         *
    +         * @param viewModel The view model instance to hold a weak reference to.
    +         */
    +        MoreOptionsButtonListener(@Nullable BiometricViewModel viewModel) {
    +            mViewModelRef = new WeakReference<>(viewModel);
    +        }
    +
    +        @Override
    +        public void onClick(DialogInterface dialogInterface, int which) {
    +            if (mViewModelRef.get() != null) {
    +                mViewModelRef.get().setMoreOptionsButtonPressPending(true);
    +            }
    +        }
    +    }
    +
    +    /**
          * The executor that will run authentication callback methods.
          *
          * 

    If unset, callbacks are invoked on the main thread with {@link Looper#getMainLooper()}. @@ -181,6 +206,11 @@ @Nullable private DialogInterface.OnClickListener mNegativeButtonListener; /** + * A dialog listener for the more options button shown on the prompt content. + */ + @Nullable private DialogInterface.OnClickListener mMoreOptionsButtonListener; + + /** * A label for the negative button shown on the prompt. * *

    If set, this value overrides the one returned by @@ -251,6 +281,11 @@ @Nullable private MutableLiveData mIsNegativeButtonPressPending; /** + * Whether the user has pressed the more options button on the prompt content. + */ + @Nullable private MutableLiveData mIsMoreOptionsButtonPressPending; + + /** * Whether the fingerprint dialog should always be dismissed instantly. */ private boolean mIsFingerprintDialogDismissedInstantly = true; @@ -327,6 +362,47 @@ } /** + * Gets the logo res to be shown on the biometric prompt. + * + *

    This method relies on the {@link BiometricPrompt.PromptInfo} set by + * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}. + * + * @return The logo res for the prompt, or -1 if not set. + */ + @SuppressLint("MissingPermission") + int getLogoRes() { + return mPromptInfo != null ? mPromptInfo.getLogoRes() : -1; + } + + /** + * Gets the logo bitmap to be shown on the biometric prompt. + * + *

    This method relies on the {@link BiometricPrompt.PromptInfo} set by + * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}. + * + * @return The logo bitmap for the prompt, or null if not set. + */ + @SuppressLint("MissingPermission") + @Nullable + Bitmap getLogoBitmap() { + return mPromptInfo != null ? mPromptInfo.getLogoBitmap() : null; + } + + /** + * Gets the logo description to be shown on the biometric prompt. + * + *

    This method relies on the {@link BiometricPrompt.PromptInfo} set by + * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}. + * + * @return The logo description for the prompt, or null if not set. + */ + @SuppressLint("MissingPermission") + @Nullable + String getLogoDescription() { + return mPromptInfo != null ? mPromptInfo.getLogoDescription() : null; + } + + /** * Gets the title to be shown on the biometric prompt. * *

    This method relies on the {@link BiometricPrompt.PromptInfo} set by @@ -366,6 +442,19 @@ } /** + * Gets the prompt content view to be shown on the biometric prompt. + * + *

    This method relies on the {@link BiometricPrompt.PromptInfo} set by + * {@link #setPromptInfo(BiometricPrompt.PromptInfo)}. + * + * @return The prompt content view for the prompt, or {@code null} if not set. + */ + @Nullable + PromptContentView getContentView() { + return mPromptInfo != null ? mPromptInfo.getContentView() : null; + } + + /** * Gets the text that should be shown for the negative button on the biometric prompt. * *

    If non-null, the value set by {@link #setNegativeButtonTextOverride(CharSequence)} is @@ -454,6 +543,14 @@ return mNegativeButtonListener; } + @NonNull + DialogInterface.OnClickListener getMoreOptionsButtonListener() { + if (mMoreOptionsButtonListener == null) { + mMoreOptionsButtonListener = new MoreOptionsButtonListener(this); + } + return mMoreOptionsButtonListener; + } + void setNegativeButtonTextOverride(@Nullable CharSequence negativeButtonTextOverride) { mNegativeButtonTextOverride = negativeButtonTextOverride; } @@ -593,6 +690,22 @@ updateValue(mIsNegativeButtonPressPending, negativeButtonPressPending); } + @NonNull + LiveData isMoreOptionsButtonPressPending() { + if (mIsMoreOptionsButtonPressPending == null) { + mIsMoreOptionsButtonPressPending = new MutableLiveData<>(); + } + return mIsMoreOptionsButtonPressPending; + } + + void setMoreOptionsButtonPressPending(boolean moreOptionsButtonPressPending) { + if (mIsMoreOptionsButtonPressPending == null) { + mIsMoreOptionsButtonPressPending = new MutableLiveData<>(); + } + updateValue(mIsMoreOptionsButtonPressPending, moreOptionsButtonPressPending); + } + + boolean isFingerprintDialogDismissedInstantly() { return mIsFingerprintDialogDismissedInstantly; }

    diff --git a/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java b/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
    index 4cc0c30..871135d 100644
    --- a/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
    +++ b/biometric/biometric/src/main/java/androidx/biometric/CryptoObjectUtils.java
    
    @@ -112,6 +112,17 @@
                 }
             }
     
    +        // Operation handle is only supported on API 35 and above.
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    +            // This should be the bottom one and only be reachable when cryptoObject was
    +            // constructed with operation handle. cryptoObject from other constructors should
    +            // already be unwrapped and returned above.
    +            final long operationHandle = Api35Impl.getOperationHandle(cryptoObject);
    +            if (operationHandle != 0) {
    +                return new BiometricPrompt.CryptoObject(operationHandle);
    +            }
    +        }
    +
             return null;
         }
     
    @@ -164,10 +175,39 @@
                 }
             }
     
    +        // Operation handle is only supported on API 35 and above.
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    +            final long operationHandle = cryptoObject.getOperationHandleCryptoObject();
    +            if (operationHandle != 0) {
    +                return Api35Impl.create(operationHandle);
    +            }
    +        }
    +
             return null;
         }
     
         /**
    +     * Get the {@code operationHandle} associated with this object or 0 if none. This needs to be
    +     * achieved by getting the corresponding
    +     * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} and then get its
    +     * operation handle.
    +     *
    +     * @param cryptoObject An instance of {@link androidx.biometric.BiometricPrompt.CryptoObject}.
    +     * @return The {@code operationHandle} associated with this object or 0 if none.
    +     */
    +    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    +    static long getOperationHandle(@Nullable BiometricPrompt.CryptoObject cryptoObject) {
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    +            final android.hardware.biometrics.BiometricPrompt.CryptoObject wrappedCryptoObject =
    +                    CryptoObjectUtils.wrapForBiometricPrompt(cryptoObject);
    +            if (wrappedCryptoObject != null) {
    +                return Api35Impl.getOperationHandle(wrappedCryptoObject);
    +            }
    +        }
    +        return 0;
    +    }
    +
    +    /**
          * Unwraps a crypto object returned by
          * {@link androidx.core.hardware.fingerprint.FingerprintManagerCompat}.
          *
    @@ -250,6 +290,11 @@
                 return null;
             }
     
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    +            Log.e(TAG, "Operation handle is not supported by FingerprintManager.");
    +            return null;
    +        }
    +
             return null;
         }
     
    @@ -298,6 +343,41 @@
         }
     
         /**
    +     * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
    +     */
    +    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    +    private static class Api35Impl {
    +        // Prevent instantiation.
    +        private Api35Impl() {}
    +
    +        /**
    +         * Creates an instance of the framework class
    +         * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} from the given
    +         * operation handle.
    +         *
    +         * @param operationHandle The operation handle to be wrapped.
    +         * @return An instance of {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.
    +         */
    +        @NonNull
    +        static android.hardware.biometrics.BiometricPrompt.CryptoObject create(
    +                long operationHandle) {
    +            return new android.hardware.biometrics.BiometricPrompt.CryptoObject(operationHandle);
    +        }
    +
    +        /**
    +         * Gets the operation handle associated with the given crypto object, if any.
    +         *
    +         * @param crypto An instance of
    +         *               {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}.
    +         * @return The wrapped operation handle object, or {@code null}.
    +         */
    +        static long getOperationHandle(
    +                @NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject crypto) {
    +            return crypto.getOperationHandle();
    +        }
    +    }
    +
    +    /**
          * Nested class to avoid verification errors for methods introduced in Android 13.0 (API 33).
          */
         @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java
    new file mode 100644
    index 0000000..099094a
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItem.java
    
    @@ -0,0 +1,23 @@
    +/*
    + * 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 androidx.biometric;
    +
    +/**
    + * An item shown on {@link PromptContentView}.
    + */
    +public interface PromptContentItem {
    +}
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.java
    new file mode 100644
    index 0000000..6ec55e2
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemBulletedText.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 androidx.biometric;
    +
    +import androidx.annotation.NonNull;
    +
    +/**
    + * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
    + */
    +public final class PromptContentItemBulletedText implements PromptContentItem {
    +    private final String mText;
    +
    +    /**
    +     * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
    +     *
    +     * @param text The text of this list item.
    +     */
    +    public PromptContentItemBulletedText(@NonNull String text) {
    +        mText = text;
    +    }
    +
    +    @NonNull
    +    String getText() {
    +        return mText;
    +    }
    +}
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.java
    new file mode 100644
    index 0000000..c7f2b6f
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentItemPlainText.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 androidx.biometric;
    +
    +import androidx.annotation.NonNull;
    +
    +/**
    + * A list item with plain text shown on {@link PromptVerticalListContentView}.
    + */
    +public final class PromptContentItemPlainText implements PromptContentItem {
    +    private final String mText;
    +
    +    /**
    +     * A list item with plain text shown on {@link PromptVerticalListContentView}.
    +     *
    +     * @param text The text of this list item.
    +     */
    +    public PromptContentItemPlainText(@NonNull String text) {
    +        mText = text;
    +    }
    +
    +    @NonNull
    +    String getText() {
    +        return mText;
    +    }
    +}
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java
    new file mode 100644
    index 0000000..090960b
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentView.java
    
    @@ -0,0 +1,23 @@
    +/*
    + * 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 androidx.biometric;
    +
    +/**
    + * Contains the information of the template of content view for Biometric Prompt.
    + */
    +public interface PromptContentView {
    +}
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java
    new file mode 100644
    index 0000000..cb6679a8
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewUtils.java
    
    @@ -0,0 +1,142 @@
    +/*
    + * 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 androidx.biometric;
    +
    +import android.annotation.SuppressLint;
    +import android.content.DialogInterface;
    +import android.os.Build;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RequiresApi;
    +
    +import java.util.concurrent.Executor;
    +
    +/**
    + * Utility class for creating and converting between different types of prompt content view that may
    + * be used internally by {@link BiometricPrompt}
    + */
    +class PromptContentViewUtils {
    +    // Prevent instantiation.
    +    private PromptContentViewUtils() {
    +    }
    +
    +    /**
    +     * Wraps a prompt content view to be passed to {@link BiometricPrompt}.
    +     *
    +     * @param contentView               An instance of {@link PromptContentView}.
    +     * @param executor                  An executor for the more options button callback.
    +     * @param moreOptionsButtonListener A listener for the more options button press event.
    +     * @return An equivalent prompt content view that is compatible with
    +     * {@link android.hardware.biometrics.PromptContentView}.
    +     */
    +    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    +    @Nullable
    +    static android.hardware.biometrics.PromptContentView wrapForBiometricPrompt(
    +            @Nullable PromptContentView contentView, @NonNull Executor executor,
    +            @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {
    +
    +        if (contentView == null) {
    +            return null;
    +        }
    +
    +        // Prompt content view is only supported on API 35 and above.
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
    +            if (contentView instanceof PromptVerticalListContentView) {
    +                return Api35Impl.createPromptVerticalListContentView(
    +                        (PromptVerticalListContentView) contentView);
    +            } else if (contentView instanceof PromptContentViewWithMoreOptionsButton) {
    +                return Api35Impl.createPromptContentViewWithMoreOptionsButton(
    +                        (PromptContentViewWithMoreOptionsButton) contentView, executor,
    +                        moreOptionsButtonListener);
    +            }
    +        }
    +
    +        return null;
    +    }
    +
    +    /**
    +     * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35).
    +     */
    +    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
    +    @SuppressLint("MissingPermission")
    +    private static class Api35Impl {
    +        // Prevent instantiation.
    +        private Api35Impl() {
    +        }
    +
    +        /**
    +         * Creates an instance of the framework class
    +         * {@link android.hardware.biometrics.PromptVerticalListContentView} from the given
    +         * content view.
    +         *
    +         * @param contentView The prompt content view to be wrapped.
    +         * @return An instance of {@link android.hardware.biometrics.PromptVerticalListContentView}.
    +         */
    +        @NonNull
    +        static android.hardware.biometrics.PromptContentView createPromptVerticalListContentView(
    +                @NonNull PromptVerticalListContentView contentView) {
    +            android.hardware.biometrics.PromptVerticalListContentView.Builder
    +                    contentViewBuilder =
    +                    new android.hardware.biometrics.PromptVerticalListContentView.Builder();
    +            if (contentView.getDescription() != null) {
    +                contentViewBuilder.setDescription(contentView.getDescription());
    +            }
    +            contentView.getListItems().forEach(
    +                    it -> {
    +                        if (it instanceof PromptContentItemPlainText) {
    +                            contentViewBuilder.addListItem(
    +                                    new android.hardware.biometrics.PromptContentItemPlainText(
    +                                            ((PromptContentItemPlainText) it).getText()));
    +                        } else if (it instanceof PromptContentItemBulletedText) {
    +                            contentViewBuilder.addListItem(
    +                                    new android.hardware.biometrics.PromptContentItemBulletedText(
    +                                            ((PromptContentItemBulletedText) it).getText()));
    +                        }
    +                    });
    +            return contentViewBuilder.build();
    +        }
    +
    +        /**
    +         * Creates an instance of the framework class
    +         * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton} from the
    +         * given content view.
    +         *
    +         * @param contentView               The prompt content view to be wrapped.
    +         * @param executor                  An executor for the more options button callback.
    +         * @param moreOptionsButtonListener A listener for the more options button press event.
    +         * @return An instance of
    +         * {@link android.hardware.biometrics.PromptContentViewWithMoreOptionsButton}.
    +         */
    +        @NonNull
    +        static android.hardware.biometrics.PromptContentView
    +                createPromptContentViewWithMoreOptionsButton(
    +                        @NonNull PromptContentViewWithMoreOptionsButton contentView,
    +                        @NonNull Executor executor,
    +                        @NonNull DialogInterface.OnClickListener moreOptionsButtonListener) {
    +            android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.Builder
    +                    contentViewBuilder =
    +                    new android.hardware.biometrics.PromptContentViewWithMoreOptionsButton
    +                            .Builder();
    +            if (contentView.getDescription() != null) {
    +                contentViewBuilder.setDescription(contentView.getDescription());
    +            }
    +            contentViewBuilder.setMoreOptionsButtonListener(executor, moreOptionsButtonListener);
    +            return contentViewBuilder.build();
    +        }
    +    }
    +}
    
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.java
    new file mode 100644
    index 0000000..eb4f9aa
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptContentViewWithMoreOptionsButton.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 androidx.biometric;
    +
    +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.annotation.RequiresPermission;
    +
    +/**
    + * Contains the information of the template of content view with a more options button for
    + * Biometric Prompt.
    + * 

    + * This button should be used to provide more options for sign in or other purposes, such as when a + * user needs to select between multiple app-specific accounts or profiles that are available for + * sign in. + *

    + * Apps should avoid using this when possible because it will create additional steps that the user + * must navigate through - clicking the more options button will dismiss the prompt, provide the app + * an opportunity to ask the user for the correct option, and finally allow the app to decide how to + * proceed once selected. + * + *

    + * Here's how you'd set a PromptContentViewWithMoreOptionsButton on a Biometric + * Prompt: + *

    
    + * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
    + *     .setTitle(...)
    + *     .setSubTitle(...)
    + *     .setContentView(
    + *         new PromptContentViewWithMoreOptionsButton.Builder()
    + *             .setDescription("test description")
    + *             .setMoreOptionsButtonListener(executor, listener)
    + *             .build()
    + *      )
    + *     .build();
    + * 
    + */ +public final class PromptContentViewWithMoreOptionsButton implements PromptContentView { + static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225; + + private final String mDescription; + + private PromptContentViewWithMoreOptionsButton(@NonNull String description) { + mDescription = description; + } + + /** + * Gets the description for the content view, as set by + * {@link PromptContentViewWithMoreOptionsButton.Builder#setDescription(String)}. + * + * @return The description for the content view, or null if the content view has no description. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @Nullable + public String getDescription() { + return mDescription; + } + + /** + * A builder used to set individual options for the + * {@link PromptContentViewWithMoreOptionsButton} class. + */ + public static final class Builder { + private String mDescription; + + /** + * Optional: Sets a description that will be shown on the content view. + * + * @param description The description to display. + * @return This builder. + * @throws IllegalArgumentException If description exceeds certain character limit. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @NonNull + public Builder setDescription(@NonNull String description) { + if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) { + throw new IllegalArgumentException("The character number of description exceeds " + + MAX_DESCRIPTION_CHARACTER_NUMBER); + } + mDescription = description; + return this; + } + + /** + * Creates a {@link PromptContentViewWithMoreOptionsButton}. + * + * @return An instance of {@link PromptContentViewWithMoreOptionsButton}. + */ + @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) + @NonNull + public PromptContentViewWithMoreOptionsButton build() { + return new PromptContentViewWithMoreOptionsButton(mDescription); + } + } +}
    diff --git a/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
    new file mode 100644
    index 0000000..58d7be2
    --- /dev/null
    +++ b/biometric/biometric/src/main/java/androidx/biometric/PromptVerticalListContentView.java
    
    @@ -0,0 +1,171 @@
    +/*
    + * 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 androidx.biometric;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +/**
    + * Contains the information of the template of vertical list content view for Biometric Prompt.
    + * 

    + * Here's how you'd set a PromptVerticalListContentView on a Biometric Prompt: + *

    
    + * BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
    + *     .setTitle(...)
    + *     .setSubTitle(...)
    + *     .setContentView(
    + *         new PromptVerticalListContentView.Builder()
    + *             .setDescription("test description")
    + *             .addListItem(new PromptContentItemPlainText("test item 1"))
    + *             .addListItem(new PromptContentItemPlainText("test item 2"))
    + *             .addListItem(new PromptContentItemBulletedText("test item 3"))
    + *             .build()
    + *      )
    + *     .build();
    + * 
    + */ +public final class PromptVerticalListContentView implements PromptContentView { + static final int MAX_ITEM_NUMBER = 20; + static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; + static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225; + + private final List mContentList; + private final String mDescription; + + private PromptVerticalListContentView(@NonNull List contentList, + @NonNull String description) { + mContentList = contentList; + mDescription = description; + } + + /** + * Gets the description for the content view, as set by + * {@link PromptVerticalListContentView.Builder#setDescription(String)}. + * + * @return The description for the content view, or null if the content view has no description. + */ + @Nullable + public String getDescription() { + return mDescription; + } + + /** + * Gets the list of items on the content view, as set by + * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}. + * + * @return The item list on the content view. + */ + @NonNull + public List getListItems() { + return new ArrayList<>(mContentList); + } + + /** + * A builder used to set individual options for the {@link PromptVerticalListContentView} class. + */ + public static final class Builder { + private final List mContentList = new ArrayList<>(); + private String mDescription; + + /** + * Optional: Sets a description that will be shown on the content view. + * + * @param description The description to display. + * @return This builder. + * @throws IllegalArgumentException If description exceeds certain character limit. + */ + @NonNull + public Builder setDescription(@NonNull String description) { + if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) { + throw new IllegalArgumentException("The character number of description exceeds " + + MAX_DESCRIPTION_CHARACTER_NUMBER); + } + mDescription = description; + return this; + } + + /** + * Optional: Adds a list item in the current row. + * + * @param listItem The list item view to display + * @return This builder. + * @throws IllegalArgumentException If this list item exceeds certain character limits or + * the number of list items exceeds certain limit. + */ + @NonNull + public Builder addListItem(@NonNull PromptContentItem listItem) { + mContentList.add(listItem); + checkItemLimits(listItem); + return this; + } + + /** + * Optional: Adds a list item in the current row. + * + * @param listItem The list item view to display + * @param index The position at which to add the item + * @return This builder. + * @throws IllegalArgumentException If this list item exceeds certain character limits or + * the number of list items exceeds certain limit. + */ + @NonNull + public Builder addListItem(@NonNull PromptContentItem listItem, int index) { + mContentList.add(index, listItem); + checkItemLimits(listItem); + return this; + } + + private void checkItemLimits(@NonNull PromptContentItem listItem) { + if (doesListItemExceedsCharLimit(listItem)) { + throw new IllegalArgumentException( + "The character number of list item exceeds " + + MAX_EACH_ITEM_CHARACTER_NUMBER); + } + if (mContentList.size() > MAX_ITEM_NUMBER) { + throw new IllegalArgumentException( + "The number of list items exceeds " + MAX_ITEM_NUMBER); + } + } + + private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) { + if (listItem instanceof PromptContentItemPlainText) { + return ((PromptContentItemPlainText) listItem).getText().length() + > MAX_EACH_ITEM_CHARACTER_NUMBER; + } else if (listItem instanceof PromptContentItemBulletedText) { + return ((PromptContentItemBulletedText) listItem).getText().length() + > MAX_EACH_ITEM_CHARACTER_NUMBER; + } else { + return false; + } + } + + + /** + * Creates a {@link PromptVerticalListContentView}. + * + * @return An instance of {@link PromptVerticalListContentView}. + */ + @NonNull + public PromptVerticalListContentView build() { + return new PromptVerticalListContentView(mContentList, mDescription); + } + } +} +
    diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
    index 56a5342..a43ef4c 100644
    --- a/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
    +++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricPromptTest.java
    
    @@ -18,6 +18,7 @@
     
     import static com.google.common.truth.Truth.assertThat;
     
    +import android.graphics.Bitmap;
     import android.os.Build;
     
     import androidx.biometric.BiometricManager.Authenticators;
    @@ -58,6 +59,95 @@
             assertThat(allowedAuthenticators).isEqualTo(allowedAuthenticators);
         }
     
    +    @Test
    +    public void testPromptInfo_CanSetAndGetOptions_logoResAndDescription() {
    +        final int logoRes = R.drawable.fingerprint_dialog_fp_icon;
    +        final String logoDescription = "logo description";
    +        final String title = "Title";
    +        final String negativeButtonText = "Negative";
    +
    +        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
    +                .setLogoRes(logoRes)
    +                .setLogoDescription(logoDescription)
    +                .setLogoDescription(logoDescription)
    +                .setTitle(title)
    +                .setNegativeButtonText(negativeButtonText)
    +                .build();
    +
    +        assertThat(info.getLogoRes()).isEqualTo(logoRes);
    +        assertThat(info.getLogoDescription()).isEqualTo(logoDescription);
    +    }
    +
    +    @Test
    +    public void testPromptInfo_CanSetAndGetOptions_logoBitmap() {
    +        final Bitmap logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565);
    +        final String logoDescription = "logo description";
    +        final String title = "Title";
    +        final String negativeButtonText = "Negative";
    +
    +        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
    +                .setLogoBitmap(logoBitmap)
    +                .setLogoDescription(logoDescription)
    +                .setTitle(title)
    +                .setNegativeButtonText(negativeButtonText)
    +                .build();
    +
    +        assertThat(info.getLogoBitmap()).isEqualTo(logoBitmap);
    +    }
    +
    +    @Test
    +    public void testPromptInfo_CanSetAndGetOptions_verticalListContent() {
    +        final String contentDescription = "test description";
    +        final String itemOne = "content item 1";
    +        final String itemTwo = "content item 2";
    +        final PromptVerticalListContentView contentView =
    +                new PromptVerticalListContentView.Builder()
    +                        .setDescription(contentDescription)
    +                        .addListItem(new PromptContentItemBulletedText(itemOne))
    +                        .addListItem(new PromptContentItemBulletedText(itemTwo), 1).build();
    +        final String title = "Title";
    +        final String negativeButtonText = "Negative";
    +
    +        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
    +                .setTitle(title)
    +                .setNegativeButtonText(negativeButtonText)
    +                .setContentView(contentView)
    +                .build();
    +
    +        assertThat(info.getContentView()).isEqualTo(contentView);
    +        final PromptVerticalListContentView realContentView =
    +                (PromptVerticalListContentView) info.getContentView();
    +        assertThat(realContentView.getDescription()).isEqualTo(contentDescription);
    +        final PromptContentItemBulletedText realItemOne =
    +                (PromptContentItemBulletedText) realContentView.getListItems().get(0);
    +        assertThat(realItemOne.getText()).isEqualTo(itemOne);
    +        final PromptContentItemBulletedText realItemTwo =
    +                (PromptContentItemBulletedText) realContentView.getListItems().get(1);
    +        assertThat(realItemTwo.getText()).isEqualTo(itemTwo);
    +
    +    }
    +
    +    @Test
    +    public void testPromptInfo_CanSetAndGetOptions_contentViewMoreOptionsButton() {
    +        final String contentDescription = "test description";
    +        final PromptContentViewWithMoreOptionsButton contentView =
    +                new PromptContentViewWithMoreOptionsButton.Builder().setDescription(
    +                        contentDescription).build();
    +        final String title = "Title";
    +        final String negativeButtonText = "Negative";
    +
    +        final BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
    +                .setTitle(title)
    +                .setNegativeButtonText(negativeButtonText)
    +                .setContentView(contentView)
    +                .build();
    +
    +        assertThat(info.getContentView()).isEqualTo(contentView);
    +        assertThat(
    +                ((PromptContentViewWithMoreOptionsButton) info.getContentView())
    +                        .getDescription()).isEqualTo(contentDescription);
    +    }
    +
         @Test(expected = IllegalArgumentException.class)
         public void testPromptInfo_FailsToBuild_WithNoTitle() {
             new BiometricPrompt.PromptInfo.Builder().setNegativeButtonText("Cancel").build();
    
    diff --git a/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java b/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
    index 51e0889..f935035 100644
    --- a/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
    +++ b/biometric/biometric/src/test/java/androidx/biometric/BiometricViewModelTest.java
    
    @@ -36,13 +36,13 @@
         }
     
         @Test
    -    public void testCanUpdateLiveDataValue_OnMainThread() {
    +    public void testCanUpdateNegativeButtonLiveDataValue_OnMainThread() {
             mViewModel.setNegativeButtonPressPending(true);
             assertThat(mViewModel.isNegativeButtonPressPending().getValue()).isTrue();
         }
     
         @Test
    -    public void testCanUpdateLiveDataValue_OnBackgroundThread() throws Exception {
    +    public void testCanUpdateNegativeButtonLiveDataValue_OnBackgroundThread() throws Exception {
             final Thread backgroundThread = new Thread(new Runnable() {
                 @Override
                 public void run() {
    @@ -54,4 +54,24 @@
             ShadowLooper.runUiThreadTasks();
             assertThat(mViewModel.isNegativeButtonPressPending().getValue()).isTrue();
         }
    +
    +    @Test
    +    public void testCanUpdateMoreOptionsButtonLiveDataValue_OnMainThread() {
    +        mViewModel.setMoreOptionsButtonPressPending(true);
    +        assertThat(mViewModel.isMoreOptionsButtonPressPending().getValue()).isTrue();
    +    }
    +
    +    @Test
    +    public void testCanUpdateMoreOptionsButtonLiveDataValue_OnBackgroundThread() throws Exception {
    +        final Thread backgroundThread = new Thread(new Runnable() {
    +            @Override
    +            public void run() {
    +                mViewModel.setMoreOptionsButtonPressPending(true);
    +            }
    +        });
    +        backgroundThread.start();
    +        backgroundThread.join();
    +        ShadowLooper.runUiThreadTasks();
    +        assertThat(mViewModel.isMoreOptionsButtonPressPending().getValue()).isTrue();
    +    }
     }
    
    diff --git a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
    index 4a62e9d..d750167 100644
    --- a/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
    +++ b/compose/material3/adaptive/adaptive/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/CollectWindowSizeAsStateTest.kt
    
    @@ -91,7 +91,8 @@
     ) : WindowMetricsCalculator {
         override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
             return WindowMetrics(
    -            Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height)
    +            Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
    +            density = 1f
             )
         }
     
    @@ -101,7 +102,8 @@
     
         override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
             return WindowMetrics(
    -            Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height)
    +            Rect(0, 0, mockWindowSize.value.width, mockWindowSize.value.height),
    +            density = 1f
             )
         }
     }
    
    diff --git a/core/core/build.gradle b/core/core/build.gradle
    index 5f369e5..4de612c 100644
    --- a/core/core/build.gradle
    +++ b/core/core/build.gradle
    
    @@ -71,6 +71,7 @@
     }
     
     android {
    +    compileSdkPreview "VanillaIceCream"
         buildFeatures {
             aidl = true
         }
    
    diff --git a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
    index d213e1f..70bf03c 100644
    --- a/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
    +++ b/core/core/src/androidTest/java/androidx/core/view/inputmethod/EditorInfoCompatTest.java
    
    @@ -25,6 +25,7 @@
     import static org.junit.Assert.assertTrue;
     import static org.junit.Assert.fail;
     
    +import android.os.Bundle;
     import android.os.Parcel;
     import android.support.v4.BaseInstrumentationTestCase;
     import android.text.SpannableStringBuilder;
    @@ -91,6 +92,8 @@
         @Test
         public void testSetStylusHandwritingEnabled() {
             EditorInfo editorInfo = new EditorInfo();
    +        assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
    +
             EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true /* enabled */);
             assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
     
    @@ -99,6 +102,45 @@
         }
     
         @Test
    +    public void testSetStylusHandwritingEnabled_compatWithCoreVersion1_13() {
    +        EditorInfo editorInfo = new EditorInfo();
    +        setStylusHandwritingEnabled_coreVersion1_13(editorInfo, false);
    +        assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
    +
    +        editorInfo = new EditorInfo();
    +        setStylusHandwritingEnabled_coreVersion1_13(editorInfo, true);
    +        assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
    +
    +        editorInfo = new EditorInfo();
    +        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, false);
    +        assertFalse(isStylusHandwritingEnabled_coreVersion1_13(editorInfo));
    +
    +        editorInfo = new EditorInfo();
    +        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true);
    +        assertTrue(isStylusHandwritingEnabled_coreVersion1_13(editorInfo));
    +    }
    +
    +    @Test
    +    @SdkSuppress(minSdkVersion = 35)
    +    public void testSetStylusHandwritingEnabled_compatWithAndroidV() {
    +        EditorInfo editorInfo = new EditorInfo();
    +        editorInfo.setStylusHandwritingEnabled(false);
    +        assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
    +
    +        editorInfo = new EditorInfo();
    +        editorInfo.setStylusHandwritingEnabled(true);
    +        assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
    +
    +        editorInfo = new EditorInfo();
    +        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, false);
    +        assertFalse(editorInfo.isStylusHandwritingEnabled());
    +
    +        editorInfo = new EditorInfo();
    +        EditorInfoCompat.setStylusHandwritingEnabled(editorInfo, true);
    +        assertTrue(editorInfo.isStylusHandwritingEnabled());
    +    }
    +
    +    @Test
         public void setInitialText_nullInputText_throwsException() {
             final CharSequence testText = null;
             final EditorInfo editorInfo = new EditorInfo();
    @@ -319,4 +361,22 @@
             }
             return builder;
         }
    +
    +    /** This is the version in AndroidX Core library 1.13. */
    +    private static void setStylusHandwritingEnabled_coreVersion1_13(
    +            EditorInfo editorInfo, boolean enabled) {
    +        if (editorInfo.extras == null) {
    +            editorInfo.extras = new Bundle();
    +        }
    +        editorInfo.extras.putBoolean(EditorInfoCompat.STYLUS_HANDWRITING_ENABLED_KEY, enabled);
    +    }
    +
    +    /** This is the version in AndroidX Core library 1.13. */
    +    public static boolean isStylusHandwritingEnabled_coreVersion1_13(EditorInfo editorInfo) {
    +        if (editorInfo.extras == null) {
    +            // disabled by default
    +            return false;
    +        }
    +        return editorInfo.extras.getBoolean(EditorInfoCompat.STYLUS_HANDWRITING_ENABLED_KEY);
    +    }
     }
    
    diff --git a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
    index 7ef503a..0686848 100644
    --- a/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
    +++ b/core/core/src/main/java/androidx/core/view/inputmethod/EditorInfoCompat.java
    
    @@ -41,6 +41,7 @@
     import androidx.annotation.Nullable;
     import androidx.annotation.RequiresApi;
     import androidx.annotation.VisibleForTesting;
    +import androidx.core.os.BuildCompat;
     import androidx.core.util.Preconditions;
     
     import java.lang.annotation.Retention;
    @@ -104,7 +105,8 @@
         private static final String CONTENT_SELECTION_END_KEY =
                 "androidx.core.view.inputmethod.EditorInfoCompat.CONTENT_SELECTION_END";
     
    -    private static final String STYLUS_HANDWRITING_ENABLED_KEY =
    +    @VisibleForTesting
    +    static final String STYLUS_HANDWRITING_ENABLED_KEY =
                 "androidx.core.view.inputmethod.EditorInfoCompat.STYLUS_HANDWRITING_ENABLED";
     
         @Retention(SOURCE)
    @@ -209,6 +211,9 @@
          */
         public static void setStylusHandwritingEnabled(@NonNull EditorInfo editorInfo,
                 boolean enabled) {
    +        if (BuildCompat.isAtLeastV()) {
    +            Api35Impl.setStylusHandwritingEnabled(editorInfo, enabled);
    +        }
             if (editorInfo.extras == null) {
                 editorInfo.extras = new Bundle();
             }
    @@ -222,11 +227,14 @@
          * @see InputMethodManager#isStylusHandwritingAvailable()
          */
         public static boolean isStylusHandwritingEnabled(@NonNull EditorInfo editorInfo) {
    -        if (editorInfo.extras == null) {
    -            // disabled by default
    -            return false;
    +        if (editorInfo.extras != null
    +                && editorInfo.extras.containsKey(STYLUS_HANDWRITING_ENABLED_KEY)) {
    +            return editorInfo.extras.getBoolean(STYLUS_HANDWRITING_ENABLED_KEY);
             }
    -        return editorInfo.extras.getBoolean(STYLUS_HANDWRITING_ENABLED_KEY);
    +        if (BuildCompat.isAtLeastV()) {
    +            return Api35Impl.isStylusHandwritingEnabled(editorInfo);
    +        }
    +        return false;
         }
     
         /**
    @@ -589,4 +597,17 @@
                 return editorInfo.getInitialTextAfterCursor(length, flags);
             }
         }
    +
    +    @RequiresApi(35)
    +    private static class Api35Impl {
    +        private Api35Impl() {}
    +
    +        static void setStylusHandwritingEnabled(@NonNull EditorInfo editorInfo, boolean enabled) {
    +            editorInfo.setStylusHandwritingEnabled(enabled);
    +        }
    +
    +        static boolean isStylusHandwritingEnabled(@NonNull EditorInfo editorInfo) {
    +            return editorInfo.isStylusHandwritingEnabled();
    +        }
    +    }
     }
    
    diff --git a/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt
    deleted file mode 100644
    index e6f50d0..0000000
    --- a/credentials/credentials-play-services-auth/api/1.3.0-beta01.txt
    +++ /dev/null
    
    @@ -1 +0,0 @@
    -// Signature format: 4.0
    
    diff --git a/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt
    deleted file mode 100644
    index e69de29..0000000
    --- a/credentials/credentials-play-services-auth/api/res-1.3.0-beta01.txt
    +++ /dev/null
    
    diff --git a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt b/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt
    deleted file mode 100644
    index e6f50d0..0000000
    --- a/credentials/credentials-play-services-auth/api/restricted_1.3.0-beta01.txt
    +++ /dev/null
    
    @@ -1 +0,0 @@
    -// Signature format: 4.0
    
    diff --git a/credentials/credentials/api/1.3.0-beta01.txt b/credentials/credentials/api/1.3.0-beta01.txt
    deleted file mode 100644
    index a0d0eda..0000000
    --- a/credentials/credentials/api/1.3.0-beta01.txt
    +++ /dev/null
    
    @@ -1,1001 +0,0 @@
    -// Signature format: 4.0
    -package androidx.credentials {
    -
    -  public final class ClearCredentialStateRequest {
    -    ctor public ClearCredentialStateRequest();
    -  }
    -
    -  public abstract class CreateCredentialRequest {
    -    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
    -    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final android.os.Bundle getCredentialData();
    -    method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
    -    method public final String? getOrigin();
    -    method public final String getType();
    -    method public final boolean isAutoSelectAllowed();
    -    method public final boolean isSystemProviderRequired();
    -    method public final boolean preferImmediatelyAvailableCredentials();
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final android.os.Bundle credentialData;
    -    property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isSystemProviderRequired;
    -    property public final String? origin;
    -    property public final boolean preferImmediatelyAvailableCredentials;
    -    property public final String type;
    -    field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
    -  }
    -
    -  public static final class CreateCredentialRequest.Companion {
    -    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
    -    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
    -  }
    -
    -  public static final class CreateCredentialRequest.DisplayInfo {
    -    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
    -    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
    -    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
    -    method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
    -    method public CharSequence? getUserDisplayName();
    -    method public CharSequence getUserId();
    -    property public final CharSequence? userDisplayName;
    -    property public final CharSequence userId;
    -    field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
    -  }
    -
    -  public static final class CreateCredentialRequest.DisplayInfo.Companion {
    -    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
    -  }
    -
    -  public abstract class CreateCredentialResponse {
    -    method public final android.os.Bundle getData();
    -    method public final String getType();
    -    property public final android.os.Bundle data;
    -    property public final String type;
    -  }
    -
    -  public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
    -  }
    -
    -  public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
    -    ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
    -  }
    -
    -  public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
    -    ctor public CreatePasswordRequest(String id, String password);
    -    ctor public CreatePasswordRequest(String id, String password, optional String? origin);
    -    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
    -    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
    -    ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
    -    method public String getId();
    -    method public String getPassword();
    -    property public final String id;
    -    property public final String password;
    -  }
    -
    -  public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
    -    ctor public CreatePasswordResponse();
    -  }
    -
    -  public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
    -    ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
    -    method public String getRegistrationResponseJson();
    -    property public final String registrationResponseJson;
    -  }
    -
    -  public abstract class Credential {
    -    method public final android.os.Bundle getData();
    -    method public final String getType();
    -    property public final android.os.Bundle data;
    -    property public final String type;
    -  }
    -
    -  public interface CredentialManager {
    -    method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation);
    -    method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method public static androidx.credentials.CredentialManager create(android.content.Context context);
    -    method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation);
    -    method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
    -    method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation);
    -    method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation);
    -    method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation);
    -    method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    field public static final androidx.credentials.CredentialManager.Companion Companion;
    -  }
    -
    -  public static final class CredentialManager.Companion {
    -    method public androidx.credentials.CredentialManager create(android.content.Context context);
    -  }
    -
    -  public interface CredentialManagerCallback {
    -    method public void onError(E e);
    -    method public void onResult(R result);
    -  }
    -
    -  public abstract class CredentialOption {
    -    method public final java.util.Set getAllowedProviders();
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final android.os.Bundle getRequestData();
    -    method public final String getType();
    -    method public final int getTypePriorityHint();
    -    method public final boolean isAutoSelectAllowed();
    -    method public final boolean isSystemProviderRequired();
    -    property public final java.util.Set allowedProviders;
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isSystemProviderRequired;
    -    property public final android.os.Bundle requestData;
    -    property public final String type;
    -    property public final int typePriorityHint;
    -    field public static final androidx.credentials.CredentialOption.Companion Companion;
    -    field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
    -    field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
    -    field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
    -    field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
    -  }
    -
    -  public static final class CredentialOption.Companion {
    -  }
    -
    -  public interface CredentialProvider {
    -    method public boolean isAvailableOnDevice();
    -    method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -  }
    -
    -  public class CustomCredential extends androidx.credentials.Credential {
    -    ctor public CustomCredential(String type, android.os.Bundle data);
    -  }
    -
    -  public final class GetCredentialRequest {
    -    ctor public GetCredentialRequest(java.util.List credentialOptions);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
    -    method public java.util.List getCredentialOptions();
    -    method public String? getOrigin();
    -    method public boolean getPreferIdentityDocUi();
    -    method public android.content.ComponentName? getPreferUiBrandingComponentName();
    -    method public boolean preferImmediatelyAvailableCredentials();
    -    property public final java.util.List credentialOptions;
    -    property public final String? origin;
    -    property public final boolean preferIdentityDocUi;
    -    property public final boolean preferImmediatelyAvailableCredentials;
    -    property public final android.content.ComponentName? preferUiBrandingComponentName;
    -  }
    -
    -  public static final class GetCredentialRequest.Builder {
    -    ctor public GetCredentialRequest.Builder();
    -    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
    -    method public androidx.credentials.GetCredentialRequest build();
    -    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List credentialOptions);
    -    method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
    -    method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
    -    method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
    -    method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
    -  }
    -
    -  public final class GetCredentialResponse {
    -    ctor public GetCredentialResponse(androidx.credentials.Credential credential);
    -    method public androidx.credentials.Credential getCredential();
    -    property public final androidx.credentials.Credential credential;
    -  }
    -
    -  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint);
    -  }
    -
    -  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
    -    ctor public GetPasswordOption();
    -    ctor public GetPasswordOption(optional java.util.Set allowedUserIds);
    -    ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed);
    -    ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);
    -    method public java.util.Set getAllowedUserIds();
    -    property public final java.util.Set allowedUserIds;
    -  }
    -
    -  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
    -    ctor public GetPublicKeyCredentialOption(String requestJson);
    -    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
    -    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set allowedProviders);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public final class PasswordCredential extends androidx.credentials.Credential {
    -    ctor public PasswordCredential(String id, String password);
    -    method public String getId();
    -    method public String getPassword();
    -    property public final String id;
    -    property public final String password;
    -    field public static final androidx.credentials.PasswordCredential.Companion Companion;
    -    field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
    -  }
    -
    -  public static final class PasswordCredential.Companion {
    -  }
    -
    -  @RequiresApi(34) public final class PrepareGetCredentialResponse {
    -    method public kotlin.jvm.functions.Function1? getCredentialTypeDelegate();
    -    method public kotlin.jvm.functions.Function0? getHasAuthResultsDelegate();
    -    method public kotlin.jvm.functions.Function0? getHasRemoteResultsDelegate();
    -    method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
    -    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
    -    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
    -    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
    -    method public boolean isNullHandlesForTest();
    -    property public final kotlin.jvm.functions.Function1? credentialTypeDelegate;
    -    property public final kotlin.jvm.functions.Function0? hasAuthResultsDelegate;
    -    property public final kotlin.jvm.functions.Function0? hasRemoteResultsDelegate;
    -    property public final boolean isNullHandlesForTest;
    -    property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
    -  }
    -
    -  @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
    -    ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
    -  }
    -
    -  @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
    -    ctor public PrepareGetCredentialResponse.TestBuilder();
    -    method public androidx.credentials.PrepareGetCredentialResponse build();
    -    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1 handler);
    -    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0 handler);
    -    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0 handler);
    -  }
    -
    -  public final class PublicKeyCredential extends androidx.credentials.Credential {
    -    ctor public PublicKeyCredential(String authenticationResponseJson);
    -    method public String getAuthenticationResponseJson();
    -    property public final String authenticationResponseJson;
    -    field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
    -    field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
    -  }
    -
    -  public static final class PublicKeyCredential.Companion {
    -  }
    -
    -}
    -
    -package androidx.credentials.exceptions {
    -
    -  public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialCustomException(String type);
    -    ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
    -    method public String getType();
    -    property public String type;
    -  }
    -
    -  public abstract class ClearCredentialException extends java.lang.Exception {
    -  }
    -
    -  public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialInterruptedException();
    -    ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialProviderConfigurationException();
    -    ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialUnknownException();
    -    ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialUnsupportedException();
    -    ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialCancellationException();
    -    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialCustomException(String type);
    -    ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
    -    method public String getType();
    -    property public String type;
    -  }
    -
    -  public abstract class CreateCredentialException extends java.lang.Exception {
    -  }
    -
    -  public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialInterruptedException();
    -    ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialNoCreateOptionException();
    -    ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialProviderConfigurationException();
    -    ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialUnknownException();
    -    ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialUnsupportedException();
    -    ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialCancellationException();
    -    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialCustomException(String type);
    -    ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
    -    method public String getType();
    -    property public String type;
    -  }
    -
    -  public abstract class GetCredentialException extends java.lang.Exception {
    -  }
    -
    -  public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialInterruptedException();
    -    ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialProviderConfigurationException();
    -    ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialUnknownException();
    -    ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialUnsupportedException();
    -    ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public NoCredentialException();
    -    ctor public NoCredentialException(optional CharSequence? errorMessage);
    -  }
    -
    -}
    -
    -package androidx.credentials.exceptions.domerrors {
    -
    -  public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public AbortError();
    -  }
    -
    -  public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public ConstraintError();
    -  }
    -
    -  public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public DataCloneError();
    -  }
    -
    -  public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public DataError();
    -  }
    -
    -  public abstract class DomError {
    -    ctor public DomError(String type);
    -  }
    -
    -  public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public EncodingError();
    -  }
    -
    -  public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public HierarchyRequestError();
    -  }
    -
    -  public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InUseAttributeError();
    -  }
    -
    -  public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidCharacterError();
    -  }
    -
    -  public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidModificationError();
    -  }
    -
    -  public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidNodeTypeError();
    -  }
    -
    -  public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidStateError();
    -  }
    -
    -  public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NamespaceError();
    -  }
    -
    -  public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NetworkError();
    -  }
    -
    -  public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NoModificationAllowedError();
    -  }
    -
    -  public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotAllowedError();
    -  }
    -
    -  public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotFoundError();
    -  }
    -
    -  public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotReadableError();
    -  }
    -
    -  public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotSupportedError();
    -  }
    -
    -  public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public OperationError();
    -  }
    -
    -  public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public OptOutError();
    -  }
    -
    -  public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public QuotaExceededError();
    -  }
    -
    -  public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public ReadOnlyError();
    -  }
    -
    -  public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public SecurityError();
    -  }
    -
    -  public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public SyntaxError();
    -  }
    -
    -  public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public TimeoutError();
    -  }
    -
    -  public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public TransactionInactiveError();
    -  }
    -
    -  public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public UnknownError();
    -  }
    -
    -  public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public VersionError();
    -  }
    -
    -  public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public WrongDocumentError();
    -  }
    -
    -}
    -
    -package androidx.credentials.exceptions.publickeycredential {
    -
    -  public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
    -    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
    -    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
    -    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
    -    property public final androidx.credentials.exceptions.domerrors.DomError domError;
    -  }
    -
    -  public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
    -  }
    -
    -  public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
    -    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
    -    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
    -    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
    -    property public final androidx.credentials.exceptions.domerrors.DomError domError;
    -  }
    -
    -  public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
    -  }
    -
    -}
    -
    -package androidx.credentials.provider {
    -
    -  public final class Action {
    -    ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
    -    method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence? getSubtitle();
    -    method public CharSequence getTitle();
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence? subtitle;
    -    property public final CharSequence title;
    -    field public static final androidx.credentials.provider.Action.Companion Companion;
    -  }
    -
    -  public static final class Action.Builder {
    -    ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.Action build();
    -    method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
    -  }
    -
    -  public static final class Action.Companion {
    -    method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
    -  }
    -
    -  public final class AuthenticationAction {
    -    ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
    -    method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence getTitle();
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence title;
    -    field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
    -  }
    -
    -  public static final class AuthenticationAction.Builder {
    -    ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.AuthenticationAction build();
    -  }
    -
    -  public static final class AuthenticationAction.Companion {
    -    method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
    -  }
    -
    -  public abstract class BeginCreateCredentialRequest {
    -    ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -    method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
    -    method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
    -    method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final String getType();
    -    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final String type;
    -    field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
    -  }
    -
    -  public static final class BeginCreateCredentialRequest.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
    -    method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public final class BeginCreateCredentialResponse {
    -    ctor public BeginCreateCredentialResponse(optional java.util.List createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
    -    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
    -    method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
    -    method public java.util.List getCreateEntries();
    -    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
    -    property public final java.util.List createEntries;
    -    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
    -    field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
    -  }
    -
    -  public static final class BeginCreateCredentialResponse.Builder {
    -    ctor public BeginCreateCredentialResponse.Builder();
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse build();
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List createEntries);
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
    -  }
    -
    -  public static final class BeginCreateCredentialResponse.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
    -    ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -  }
    -
    -  public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
    -    ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
    -  }
    -
    -  public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
    -    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
    -    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
    -    method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public abstract class BeginGetCredentialOption {
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final String getId();
    -    method public final String getType();
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final String id;
    -    property public final String type;
    -  }
    -
    -  public final class BeginGetCredentialRequest {
    -    ctor public BeginGetCredentialRequest(java.util.List beginGetCredentialOptions);
    -    ctor public BeginGetCredentialRequest(java.util.List beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
    -    method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
    -    method public java.util.List getBeginGetCredentialOptions();
    -    method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
    -    property public final java.util.List beginGetCredentialOptions;
    -    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
    -    field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
    -  }
    -
    -  public static final class BeginGetCredentialRequest.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
    -    method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public final class BeginGetCredentialResponse {
    -    ctor public BeginGetCredentialResponse(optional java.util.List credentialEntries, optional java.util.List actions, optional java.util.List authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
    -    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
    -    method public java.util.List getActions();
    -    method public java.util.List getAuthenticationActions();
    -    method public java.util.List getCredentialEntries();
    -    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
    -    property public final java.util.List actions;
    -    property public final java.util.List authenticationActions;
    -    property public final java.util.List credentialEntries;
    -    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
    -    field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
    -  }
    -
    -  public static final class BeginGetCredentialResponse.Builder {
    -    ctor public BeginGetCredentialResponse.Builder();
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse build();
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List actions);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List authenticationEntries);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List entries);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
    -  }
    -
    -  public static final class BeginGetCredentialResponse.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
    -    ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
    -  }
    -
    -  public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
    -    ctor public BeginGetPasswordOption(java.util.Set allowedUserIds, android.os.Bundle candidateQueryData, String id);
    -    method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
    -    method public java.util.Set getAllowedUserIds();
    -    property public final java.util.Set allowedUserIds;
    -  }
    -
    -  public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
    -    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
    -    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public final class CallingAppInfo {
    -    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
    -    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
    -    method public String? getOrigin(String privilegedAllowlist);
    -    method public String getPackageName();
    -    method public android.content.pm.SigningInfo getSigningInfo();
    -    method public boolean isOriginPopulated();
    -    property public final String packageName;
    -    property public final android.content.pm.SigningInfo signingInfo;
    -  }
    -
    -  @RequiresApi(26) public final class CreateEntry {
    -    ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
    -    method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
    -    method public CharSequence getAccountName();
    -    method public CharSequence? getDescription();
    -    method public android.graphics.drawable.Icon? getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public Integer? getPasswordCredentialCount();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public Integer? getPublicKeyCredentialCount();
    -    method public Integer? getTotalCredentialCount();
    -    method public boolean isAutoSelectAllowed();
    -    property public final CharSequence accountName;
    -    property public final CharSequence? description;
    -    property public final android.graphics.drawable.Icon? icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
    -  }
    -
    -  public static final class CreateEntry.Builder {
    -    ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.CreateEntry build();
    -    method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
    -    method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
    -    method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -    method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
    -    method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
    -    method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
    -  }
    -
    -  public static final class CreateEntry.Companion {
    -    method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
    -  }
    -
    -  public abstract class CredentialEntry {
    -    method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public final CharSequence? getAffiliatedDomain();
    -    method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
    -    method public final CharSequence getEntryGroupId();
    -    method public final boolean isDefaultIconPreferredAsSingleProvider();
    -    property public final CharSequence? affiliatedDomain;
    -    property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
    -    property public final CharSequence entryGroupId;
    -    property public final boolean isDefaultIconPreferredAsSingleProvider;
    -    field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class CredentialEntry.Companion {
    -    method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
    -    ctor public CredentialProviderService();
    -    method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -  }
    -
    -  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    -    ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
    -    ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
    -    method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public android.graphics.drawable.Icon getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence? getSubtitle();
    -    method public CharSequence getTitle();
    -    method public String getType();
    -    method public CharSequence? getTypeDisplayName();
    -    method public boolean hasDefaultIcon();
    -    method public boolean isAutoSelectAllowed();
    -    method public boolean isAutoSelectAllowedFromOption();
    -    property public final boolean hasDefaultIcon;
    -    property public final android.graphics.drawable.Icon icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isAutoSelectAllowedFromOption;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence? subtitle;
    -    property public final CharSequence title;
    -    property public String type;
    -    property public final CharSequence? typeDisplayName;
    -    field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class CustomCredentialEntry.Builder {
    -    ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
    -    method public androidx.credentials.provider.CustomCredentialEntry build();
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
    -  }
    -
    -  public static final class CustomCredentialEntry.Companion {
    -    method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  public final class IntentHandlerConverters {
    -    method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
    -  }
    -
    -  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    -    ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
    -    ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
    -    method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public CharSequence? getDisplayName();
    -    method public android.graphics.drawable.Icon getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence getTypeDisplayName();
    -    method public CharSequence getUsername();
    -    method public boolean hasDefaultIcon();
    -    method public boolean isAutoSelectAllowed();
    -    method public boolean isAutoSelectAllowedFromOption();
    -    property public final CharSequence? displayName;
    -    property public final boolean hasDefaultIcon;
    -    property public final android.graphics.drawable.Icon icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isAutoSelectAllowedFromOption;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence typeDisplayName;
    -    property public final CharSequence username;
    -    field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class PasswordCredentialEntry.Builder {
    -    ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
    -    method public androidx.credentials.provider.PasswordCredentialEntry build();
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -  }
    -
    -  public static final class PasswordCredentialEntry.Companion {
    -    method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  @RequiresApi(34) public final class PendingIntentHandler {
    -    ctor public PendingIntentHandler();
    -    method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
    -    method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
    -    method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
    -    method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
    -    method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
    -    method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
    -    method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
    -    field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
    -  }
    -
    -  public static final class PendingIntentHandler.Companion {
    -    method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
    -    method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
    -    method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
    -    method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
    -    method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
    -    method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
    -    method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
    -  }
    -
    -  public final class ProviderClearCredentialStateRequest {
    -    ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
    -    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
    -    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
    -  }
    -
    -  public final class ProviderCreateCredentialRequest {
    -    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
    -    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
    -    method public androidx.credentials.CreateCredentialRequest getCallingRequest();
    -    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
    -    property public final androidx.credentials.CreateCredentialRequest callingRequest;
    -  }
    -
    -  public final class ProviderGetCredentialRequest {
    -    ctor public ProviderGetCredentialRequest(java.util.List credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
    -    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
    -    method public java.util.List getCredentialOptions();
    -    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
    -    property public final java.util.List credentialOptions;
    -  }
    -
    -  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    -    ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
    -    ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
    -    method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public CharSequence? getDisplayName();
    -    method public android.graphics.drawable.Icon getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence getTypeDisplayName();
    -    method public CharSequence getUsername();
    -    method public boolean hasDefaultIcon();
    -    method public boolean isAutoSelectAllowed();
    -    method public boolean isAutoSelectAllowedFromOption();
    -    property public final CharSequence? displayName;
    -    property public final boolean hasDefaultIcon;
    -    property public final android.graphics.drawable.Icon icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isAutoSelectAllowedFromOption;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence typeDisplayName;
    -    property public final CharSequence username;
    -    field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class PublicKeyCredentialEntry.Builder {
    -    ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry build();
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -  }
    -
    -  public static final class PublicKeyCredentialEntry.Companion {
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  public final class RemoteEntry {
    -    ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
    -    method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
    -    method public android.app.PendingIntent getPendingIntent();
    -    property public final android.app.PendingIntent pendingIntent;
    -    field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
    -  }
    -
    -  public static final class RemoteEntry.Builder {
    -    ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.RemoteEntry build();
    -  }
    -
    -  public static final class RemoteEntry.Companion {
    -    method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
    -  }
    -
    -}
    -
    
    diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
    index a0d0eda..f899a45 100644
    --- a/credentials/credentials/api/current.txt
    +++ b/credentials/credentials/api/current.txt
    
    @@ -133,6 +133,11 @@
         method public void onResult(R result);
       }
     
    +  public final class CredentialManagerViewHandler {
    +    method public static androidx.credentials.PendingGetCredentialRequest? getPendingGetCredentialRequest(android.view.View);
    +    method public static void setPendingGetCredentialRequest(android.view.View, androidx.credentials.PendingGetCredentialRequest?);
    +  }
    +
       public abstract class CredentialOption {
         method public final java.util.Set getAllowedProviders();
         method public final android.os.Bundle getCandidateQueryData();
    @@ -245,6 +250,14 @@
       public static final class PasswordCredential.Companion {
       }
     
    +  public final class PendingGetCredentialRequest {
    +    ctor public PendingGetCredentialRequest(androidx.credentials.GetCredentialRequest request, kotlin.jvm.functions.Function1 callback);
    +    method public kotlin.jvm.functions.Function1 getCallback();
    +    method public androidx.credentials.GetCredentialRequest getRequest();
    +    property public final kotlin.jvm.functions.Function1 callback;
    +    property public final androidx.credentials.GetCredentialRequest request;
    +  }
    +
       @RequiresApi(34) public final class PrepareGetCredentialResponse {
         method public kotlin.jvm.functions.Function1? getCredentialTypeDelegate();
         method public kotlin.jvm.functions.Function0? getHasAuthResultsDelegate();
    @@ -740,7 +753,7 @@
         property public final android.content.pm.SigningInfo signingInfo;
       }
     
    -  @RequiresApi(26) public final class CreateEntry {
    +  @RequiresApi(23) public final class CreateEntry {
         ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
         method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
         method public CharSequence getAccountName();
    @@ -804,7 +817,7 @@
         method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
       }
     
    -  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    +  @RequiresApi(23) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
         ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
         ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
         method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    @@ -855,7 +868,7 @@
         method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
       }
     
    -  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    +  @RequiresApi(23) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
         ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
         ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
         method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    @@ -941,7 +954,7 @@
         property public final java.util.List credentialOptions;
       }
     
    -  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    +  @RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
         ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
         ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
         method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    
    diff --git a/credentials/credentials/api/res-1.3.0-beta01.txt b/credentials/credentials/api/res-1.3.0-beta01.txt
    deleted file mode 100644
    index e69de29..0000000
    --- a/credentials/credentials/api/res-1.3.0-beta01.txt
    +++ /dev/null
    
    diff --git a/credentials/credentials/api/restricted_1.3.0-beta01.txt b/credentials/credentials/api/restricted_1.3.0-beta01.txt
    deleted file mode 100644
    index a0d0eda..0000000
    --- a/credentials/credentials/api/restricted_1.3.0-beta01.txt
    +++ /dev/null
    
    @@ -1,1001 +0,0 @@
    -// Signature format: 4.0
    -package androidx.credentials {
    -
    -  public final class ClearCredentialStateRequest {
    -    ctor public ClearCredentialStateRequest();
    -  }
    -
    -  public abstract class CreateCredentialRequest {
    -    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
    -    method @RequiresApi(23) public static final androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final android.os.Bundle getCredentialData();
    -    method public final androidx.credentials.CreateCredentialRequest.DisplayInfo getDisplayInfo();
    -    method public final String? getOrigin();
    -    method public final String getType();
    -    method public final boolean isAutoSelectAllowed();
    -    method public final boolean isSystemProviderRequired();
    -    method public final boolean preferImmediatelyAvailableCredentials();
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final android.os.Bundle credentialData;
    -    property public final androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isSystemProviderRequired;
    -    property public final String? origin;
    -    property public final boolean preferImmediatelyAvailableCredentials;
    -    property public final String type;
    -    field public static final androidx.credentials.CreateCredentialRequest.Companion Companion;
    -  }
    -
    -  public static final class CreateCredentialRequest.Companion {
    -    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider);
    -    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest createFrom(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean requireSystemProvider, optional String? origin);
    -  }
    -
    -  public static final class CreateCredentialRequest.DisplayInfo {
    -    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId);
    -    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, optional CharSequence? userDisplayName);
    -    ctor public CreateCredentialRequest.DisplayInfo(CharSequence userId, CharSequence? userDisplayName, String? preferDefaultProvider);
    -    method @RequiresApi(23) public static androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
    -    method public CharSequence? getUserDisplayName();
    -    method public CharSequence getUserId();
    -    property public final CharSequence? userDisplayName;
    -    property public final CharSequence userId;
    -    field public static final androidx.credentials.CreateCredentialRequest.DisplayInfo.Companion Companion;
    -  }
    -
    -  public static final class CreateCredentialRequest.DisplayInfo.Companion {
    -    method @RequiresApi(23) public androidx.credentials.CreateCredentialRequest.DisplayInfo createFrom(android.os.Bundle from);
    -  }
    -
    -  public abstract class CreateCredentialResponse {
    -    method public final android.os.Bundle getData();
    -    method public final String getType();
    -    property public final android.os.Bundle data;
    -    property public final String type;
    -  }
    -
    -  public class CreateCustomCredentialRequest extends androidx.credentials.CreateCredentialRequest {
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo);
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed);
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin);
    -    ctor public CreateCustomCredentialRequest(String type, android.os.Bundle credentialData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, androidx.credentials.CreateCredentialRequest.DisplayInfo displayInfo, optional boolean isAutoSelectAllowed, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
    -  }
    -
    -  public class CreateCustomCredentialResponse extends androidx.credentials.CreateCredentialResponse {
    -    ctor public CreateCustomCredentialResponse(String type, android.os.Bundle data);
    -  }
    -
    -  public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
    -    ctor public CreatePasswordRequest(String id, String password);
    -    ctor public CreatePasswordRequest(String id, String password, optional String? origin);
    -    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials);
    -    ctor public CreatePasswordRequest(String id, String password, optional String? origin, optional boolean preferImmediatelyAvailableCredentials, optional boolean isAutoSelectAllowed);
    -    ctor public CreatePasswordRequest(String id, String password, String? origin, String? preferDefaultProvider, boolean preferImmediatelyAvailableCredentials, boolean isAutoSelectAllowed);
    -    method public String getId();
    -    method public String getPassword();
    -    property public final String id;
    -    property public final String password;
    -  }
    -
    -  public final class CreatePasswordResponse extends androidx.credentials.CreateCredentialResponse {
    -    ctor public CreatePasswordResponse();
    -  }
    -
    -  public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional byte[]? clientDataHash, optional boolean preferImmediatelyAvailableCredentials, optional String? origin, optional boolean isAutoSelectAllowed);
    -    ctor public CreatePublicKeyCredentialRequest(String requestJson, byte[]? clientDataHash, boolean preferImmediatelyAvailableCredentials, String? origin, String? preferDefaultProvider, boolean isAutoSelectAllowed);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public final class CreatePublicKeyCredentialResponse extends androidx.credentials.CreateCredentialResponse {
    -    ctor public CreatePublicKeyCredentialResponse(String registrationResponseJson);
    -    method public String getRegistrationResponseJson();
    -    property public final String registrationResponseJson;
    -  }
    -
    -  public abstract class Credential {
    -    method public final android.os.Bundle getData();
    -    method public final String getType();
    -    property public final android.os.Bundle data;
    -    property public final String type;
    -  }
    -
    -  public interface CredentialManager {
    -    method public default suspend Object? clearCredentialState(androidx.credentials.ClearCredentialStateRequest request, kotlin.coroutines.Continuation);
    -    method public void clearCredentialStateAsync(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method public static androidx.credentials.CredentialManager create(android.content.Context context);
    -    method public default suspend Object? createCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, kotlin.coroutines.Continuation);
    -    method public void createCredentialAsync(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public android.app.PendingIntent createSettingsPendingIntent();
    -    method public default suspend Object? getCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation);
    -    method @RequiresApi(34) public default suspend Object? getCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, kotlin.coroutines.Continuation);
    -    method public void getCredentialAsync(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public void getCredentialAsync(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public default suspend Object? prepareGetCredential(androidx.credentials.GetCredentialRequest request, kotlin.coroutines.Continuation);
    -    method @RequiresApi(34) public void prepareGetCredentialAsync(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    field public static final androidx.credentials.CredentialManager.Companion Companion;
    -  }
    -
    -  public static final class CredentialManager.Companion {
    -    method public androidx.credentials.CredentialManager create(android.content.Context context);
    -  }
    -
    -  public interface CredentialManagerCallback {
    -    method public void onError(E e);
    -    method public void onResult(R result);
    -  }
    -
    -  public abstract class CredentialOption {
    -    method public final java.util.Set getAllowedProviders();
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final android.os.Bundle getRequestData();
    -    method public final String getType();
    -    method public final int getTypePriorityHint();
    -    method public final boolean isAutoSelectAllowed();
    -    method public final boolean isSystemProviderRequired();
    -    property public final java.util.Set allowedProviders;
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isSystemProviderRequired;
    -    property public final android.os.Bundle requestData;
    -    property public final String type;
    -    property public final int typePriorityHint;
    -    field public static final androidx.credentials.CredentialOption.Companion Companion;
    -    field public static final int PRIORITY_DEFAULT = 2000; // 0x7d0
    -    field public static final int PRIORITY_OIDC_OR_SIMILAR = 500; // 0x1f4
    -    field public static final int PRIORITY_PASSKEY_OR_SIMILAR = 100; // 0x64
    -    field public static final int PRIORITY_PASSWORD_OR_SIMILAR = 1000; // 0x3e8
    -  }
    -
    -  public static final class CredentialOption.Companion {
    -  }
    -
    -  public interface CredentialProvider {
    -    method public boolean isAvailableOnDevice();
    -    method public void onClearCredential(androidx.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method public void onCreateCredential(android.content.Context context, androidx.credentials.CreateCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method public void onGetCredential(android.content.Context context, androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public default void onGetCredential(android.content.Context context, androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle pendingGetCredentialHandle, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -    method @RequiresApi(34) public default void onPrepareCredential(androidx.credentials.GetCredentialRequest request, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Executor executor, androidx.credentials.CredentialManagerCallback callback);
    -  }
    -
    -  public class CustomCredential extends androidx.credentials.Credential {
    -    ctor public CustomCredential(String type, android.os.Bundle data);
    -  }
    -
    -  public final class GetCredentialRequest {
    -    ctor public GetCredentialRequest(java.util.List credentialOptions);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin, optional boolean preferIdentityDocUi);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName);
    -    ctor public GetCredentialRequest(java.util.List credentialOptions, optional String? origin, optional boolean preferIdentityDocUi, optional android.content.ComponentName? preferUiBrandingComponentName, optional boolean preferImmediatelyAvailableCredentials);
    -    method public java.util.List getCredentialOptions();
    -    method public String? getOrigin();
    -    method public boolean getPreferIdentityDocUi();
    -    method public android.content.ComponentName? getPreferUiBrandingComponentName();
    -    method public boolean preferImmediatelyAvailableCredentials();
    -    property public final java.util.List credentialOptions;
    -    property public final String? origin;
    -    property public final boolean preferIdentityDocUi;
    -    property public final boolean preferImmediatelyAvailableCredentials;
    -    property public final android.content.ComponentName? preferUiBrandingComponentName;
    -  }
    -
    -  public static final class GetCredentialRequest.Builder {
    -    ctor public GetCredentialRequest.Builder();
    -    method public androidx.credentials.GetCredentialRequest.Builder addCredentialOption(androidx.credentials.CredentialOption credentialOption);
    -    method public androidx.credentials.GetCredentialRequest build();
    -    method public androidx.credentials.GetCredentialRequest.Builder setCredentialOptions(java.util.List credentialOptions);
    -    method public androidx.credentials.GetCredentialRequest.Builder setOrigin(String origin);
    -    method public androidx.credentials.GetCredentialRequest.Builder setPreferIdentityDocUi(boolean preferIdentityDocUi);
    -    method public androidx.credentials.GetCredentialRequest.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
    -    method public androidx.credentials.GetCredentialRequest.Builder setPreferUiBrandingComponentName(android.content.ComponentName? component);
    -  }
    -
    -  public final class GetCredentialResponse {
    -    ctor public GetCredentialResponse(androidx.credentials.Credential credential);
    -    method public androidx.credentials.Credential getCredential();
    -    property public final androidx.credentials.Credential credential;
    -  }
    -
    -  public class GetCustomCredentialOption extends androidx.credentials.CredentialOption {
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired);
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed);
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);
    -    ctor public GetCustomCredentialOption(String type, android.os.Bundle requestData, android.os.Bundle candidateQueryData, boolean isSystemProviderRequired, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders, optional int typePriorityHint);
    -  }
    -
    -  public final class GetPasswordOption extends androidx.credentials.CredentialOption {
    -    ctor public GetPasswordOption();
    -    ctor public GetPasswordOption(optional java.util.Set allowedUserIds);
    -    ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed);
    -    ctor public GetPasswordOption(optional java.util.Set allowedUserIds, optional boolean isAutoSelectAllowed, optional java.util.Set allowedProviders);
    -    method public java.util.Set getAllowedUserIds();
    -    property public final java.util.Set allowedUserIds;
    -  }
    -
    -  public final class GetPublicKeyCredentialOption extends androidx.credentials.CredentialOption {
    -    ctor public GetPublicKeyCredentialOption(String requestJson);
    -    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash);
    -    ctor public GetPublicKeyCredentialOption(String requestJson, optional byte[]? clientDataHash, optional java.util.Set allowedProviders);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public final class PasswordCredential extends androidx.credentials.Credential {
    -    ctor public PasswordCredential(String id, String password);
    -    method public String getId();
    -    method public String getPassword();
    -    property public final String id;
    -    property public final String password;
    -    field public static final androidx.credentials.PasswordCredential.Companion Companion;
    -    field public static final String TYPE_PASSWORD_CREDENTIAL = "android.credentials.TYPE_PASSWORD_CREDENTIAL";
    -  }
    -
    -  public static final class PasswordCredential.Companion {
    -  }
    -
    -  @RequiresApi(34) public final class PrepareGetCredentialResponse {
    -    method public kotlin.jvm.functions.Function1? getCredentialTypeDelegate();
    -    method public kotlin.jvm.functions.Function0? getHasAuthResultsDelegate();
    -    method public kotlin.jvm.functions.Function0? getHasRemoteResultsDelegate();
    -    method public androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? getPendingGetCredentialHandle();
    -    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults();
    -    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(String credentialType);
    -    method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults();
    -    method public boolean isNullHandlesForTest();
    -    property public final kotlin.jvm.functions.Function1? credentialTypeDelegate;
    -    property public final kotlin.jvm.functions.Function0? hasAuthResultsDelegate;
    -    property public final kotlin.jvm.functions.Function0? hasRemoteResultsDelegate;
    -    property public final boolean isNullHandlesForTest;
    -    property public final androidx.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? pendingGetCredentialHandle;
    -  }
    -
    -  @RequiresApi(34) public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle {
    -    ctor public PrepareGetCredentialResponse.PendingGetCredentialHandle(android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle? frameworkHandle);
    -  }
    -
    -  @VisibleForTesting public static final class PrepareGetCredentialResponse.TestBuilder {
    -    ctor public PrepareGetCredentialResponse.TestBuilder();
    -    method public androidx.credentials.PrepareGetCredentialResponse build();
    -    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setCredentialTypeDelegate(kotlin.jvm.functions.Function1 handler);
    -    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasAuthResultsDelegate(kotlin.jvm.functions.Function0 handler);
    -    method @VisibleForTesting public androidx.credentials.PrepareGetCredentialResponse.TestBuilder setHasRemoteResultsDelegate(kotlin.jvm.functions.Function0 handler);
    -  }
    -
    -  public final class PublicKeyCredential extends androidx.credentials.Credential {
    -    ctor public PublicKeyCredential(String authenticationResponseJson);
    -    method public String getAuthenticationResponseJson();
    -    property public final String authenticationResponseJson;
    -    field public static final androidx.credentials.PublicKeyCredential.Companion Companion;
    -    field public static final String TYPE_PUBLIC_KEY_CREDENTIAL = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
    -  }
    -
    -  public static final class PublicKeyCredential.Companion {
    -  }
    -
    -}
    -
    -package androidx.credentials.exceptions {
    -
    -  public final class ClearCredentialCustomException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialCustomException(String type);
    -    ctor public ClearCredentialCustomException(String type, optional CharSequence? errorMessage);
    -    method public String getType();
    -    property public String type;
    -  }
    -
    -  public abstract class ClearCredentialException extends java.lang.Exception {
    -  }
    -
    -  public final class ClearCredentialInterruptedException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialInterruptedException();
    -    ctor public ClearCredentialInterruptedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class ClearCredentialProviderConfigurationException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialProviderConfigurationException();
    -    ctor public ClearCredentialProviderConfigurationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class ClearCredentialUnknownException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialUnknownException();
    -    ctor public ClearCredentialUnknownException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class ClearCredentialUnsupportedException extends androidx.credentials.exceptions.ClearCredentialException {
    -    ctor public ClearCredentialUnsupportedException();
    -    ctor public ClearCredentialUnsupportedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialCancellationException();
    -    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialCustomException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialCustomException(String type);
    -    ctor public CreateCredentialCustomException(String type, optional CharSequence? errorMessage);
    -    method public String getType();
    -    property public String type;
    -  }
    -
    -  public abstract class CreateCredentialException extends java.lang.Exception {
    -  }
    -
    -  public final class CreateCredentialInterruptedException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialInterruptedException();
    -    ctor public CreateCredentialInterruptedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialNoCreateOptionException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialNoCreateOptionException();
    -    ctor public CreateCredentialNoCreateOptionException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialProviderConfigurationException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialProviderConfigurationException();
    -    ctor public CreateCredentialProviderConfigurationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialUnknownException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialUnknownException();
    -    ctor public CreateCredentialUnknownException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class CreateCredentialUnsupportedException extends androidx.credentials.exceptions.CreateCredentialException {
    -    ctor public CreateCredentialUnsupportedException();
    -    ctor public CreateCredentialUnsupportedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialCancellationException();
    -    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialCustomException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialCustomException(String type);
    -    ctor public GetCredentialCustomException(String type, optional CharSequence? errorMessage);
    -    method public String getType();
    -    property public String type;
    -  }
    -
    -  public abstract class GetCredentialException extends java.lang.Exception {
    -  }
    -
    -  public final class GetCredentialInterruptedException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialInterruptedException();
    -    ctor public GetCredentialInterruptedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialProviderConfigurationException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialProviderConfigurationException();
    -    ctor public GetCredentialProviderConfigurationException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialUnknownException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialUnknownException();
    -    ctor public GetCredentialUnknownException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class GetCredentialUnsupportedException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public GetCredentialUnsupportedException();
    -    ctor public GetCredentialUnsupportedException(optional CharSequence? errorMessage);
    -  }
    -
    -  public final class NoCredentialException extends androidx.credentials.exceptions.GetCredentialException {
    -    ctor public NoCredentialException();
    -    ctor public NoCredentialException(optional CharSequence? errorMessage);
    -  }
    -
    -}
    -
    -package androidx.credentials.exceptions.domerrors {
    -
    -  public final class AbortError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public AbortError();
    -  }
    -
    -  public final class ConstraintError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public ConstraintError();
    -  }
    -
    -  public final class DataCloneError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public DataCloneError();
    -  }
    -
    -  public final class DataError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public DataError();
    -  }
    -
    -  public abstract class DomError {
    -    ctor public DomError(String type);
    -  }
    -
    -  public final class EncodingError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public EncodingError();
    -  }
    -
    -  public final class HierarchyRequestError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public HierarchyRequestError();
    -  }
    -
    -  public final class InUseAttributeError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InUseAttributeError();
    -  }
    -
    -  public final class InvalidCharacterError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidCharacterError();
    -  }
    -
    -  public final class InvalidModificationError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidModificationError();
    -  }
    -
    -  public final class InvalidNodeTypeError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidNodeTypeError();
    -  }
    -
    -  public final class InvalidStateError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public InvalidStateError();
    -  }
    -
    -  public final class NamespaceError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NamespaceError();
    -  }
    -
    -  public final class NetworkError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NetworkError();
    -  }
    -
    -  public final class NoModificationAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NoModificationAllowedError();
    -  }
    -
    -  public final class NotAllowedError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotAllowedError();
    -  }
    -
    -  public final class NotFoundError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotFoundError();
    -  }
    -
    -  public final class NotReadableError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotReadableError();
    -  }
    -
    -  public final class NotSupportedError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public NotSupportedError();
    -  }
    -
    -  public final class OperationError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public OperationError();
    -  }
    -
    -  public final class OptOutError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public OptOutError();
    -  }
    -
    -  public final class QuotaExceededError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public QuotaExceededError();
    -  }
    -
    -  public final class ReadOnlyError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public ReadOnlyError();
    -  }
    -
    -  public final class SecurityError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public SecurityError();
    -  }
    -
    -  public final class SyntaxError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public SyntaxError();
    -  }
    -
    -  public final class TimeoutError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public TimeoutError();
    -  }
    -
    -  public final class TransactionInactiveError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public TransactionInactiveError();
    -  }
    -
    -  public final class UnknownError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public UnknownError();
    -  }
    -
    -  public final class VersionError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public VersionError();
    -  }
    -
    -  public final class WrongDocumentError extends androidx.credentials.exceptions.domerrors.DomError {
    -    ctor public WrongDocumentError();
    -  }
    -
    -}
    -
    -package androidx.credentials.exceptions.publickeycredential {
    -
    -  public final class CreatePublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException {
    -    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
    -    ctor public CreatePublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
    -    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
    -    property public final androidx.credentials.exceptions.domerrors.DomError domError;
    -  }
    -
    -  public class CreatePublicKeyCredentialException extends androidx.credentials.exceptions.CreateCredentialException {
    -  }
    -
    -  public final class GetPublicKeyCredentialDomException extends androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialException {
    -    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError);
    -    ctor public GetPublicKeyCredentialDomException(androidx.credentials.exceptions.domerrors.DomError domError, optional CharSequence? errorMessage);
    -    method public androidx.credentials.exceptions.domerrors.DomError getDomError();
    -    property public final androidx.credentials.exceptions.domerrors.DomError domError;
    -  }
    -
    -  public class GetPublicKeyCredentialException extends androidx.credentials.exceptions.GetCredentialException {
    -  }
    -
    -}
    -
    -package androidx.credentials.provider {
    -
    -  public final class Action {
    -    ctor public Action(CharSequence title, android.app.PendingIntent pendingIntent, optional CharSequence? subtitle);
    -    method public static androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence? getSubtitle();
    -    method public CharSequence getTitle();
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence? subtitle;
    -    property public final CharSequence title;
    -    field public static final androidx.credentials.provider.Action.Companion Companion;
    -  }
    -
    -  public static final class Action.Builder {
    -    ctor public Action.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.Action build();
    -    method public androidx.credentials.provider.Action.Builder setSubtitle(CharSequence? subtitle);
    -  }
    -
    -  public static final class Action.Companion {
    -    method public androidx.credentials.provider.Action? fromAction(android.service.credentials.Action action);
    -  }
    -
    -  public final class AuthenticationAction {
    -    ctor public AuthenticationAction(CharSequence title, android.app.PendingIntent pendingIntent);
    -    method @RequiresApi(34) public static androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence getTitle();
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence title;
    -    field public static final androidx.credentials.provider.AuthenticationAction.Companion Companion;
    -  }
    -
    -  public static final class AuthenticationAction.Builder {
    -    ctor public AuthenticationAction.Builder(CharSequence title, android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.AuthenticationAction build();
    -  }
    -
    -  public static final class AuthenticationAction.Companion {
    -    method @RequiresApi(34) public androidx.credentials.provider.AuthenticationAction? fromAction(android.service.credentials.Action authenticationAction);
    -  }
    -
    -  public abstract class BeginCreateCredentialRequest {
    -    ctor public BeginCreateCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -    method public static final android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
    -    method public static final androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
    -    method public final androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final String getType();
    -    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final String type;
    -    field public static final androidx.credentials.provider.BeginCreateCredentialRequest.Companion Companion;
    -  }
    -
    -  public static final class BeginCreateCredentialRequest.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialRequest request);
    -    method public androidx.credentials.provider.BeginCreateCredentialRequest? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public final class BeginCreateCredentialResponse {
    -    ctor public BeginCreateCredentialResponse(optional java.util.List createEntries, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
    -    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
    -    method public static androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
    -    method public java.util.List getCreateEntries();
    -    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
    -    property public final java.util.List createEntries;
    -    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
    -    field public static final androidx.credentials.provider.BeginCreateCredentialResponse.Companion Companion;
    -  }
    -
    -  public static final class BeginCreateCredentialResponse.Builder {
    -    ctor public BeginCreateCredentialResponse.Builder();
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder addCreateEntry(androidx.credentials.provider.CreateEntry createEntry);
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse build();
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setCreateEntries(java.util.List createEntries);
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
    -  }
    -
    -  public static final class BeginCreateCredentialResponse.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginCreateCredentialResponse response);
    -    method public androidx.credentials.provider.BeginCreateCredentialResponse? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public class BeginCreateCustomCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
    -    ctor public BeginCreateCustomCredentialRequest(String type, android.os.Bundle candidateQueryData, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -  }
    -
    -  public final class BeginCreatePasswordCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
    -    ctor public BeginCreatePasswordCredentialRequest(androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
    -  }
    -
    -  public final class BeginCreatePublicKeyCredentialRequest extends androidx.credentials.provider.BeginCreateCredentialRequest {
    -    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData);
    -    ctor public BeginCreatePublicKeyCredentialRequest(String requestJson, androidx.credentials.provider.CallingAppInfo? callingAppInfo, android.os.Bundle candidateQueryData, optional byte[]? clientDataHash);
    -    method @VisibleForTesting public static androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest createForTest(android.os.Bundle data, androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public abstract class BeginGetCredentialOption {
    -    method public final android.os.Bundle getCandidateQueryData();
    -    method public final String getId();
    -    method public final String getType();
    -    property public final android.os.Bundle candidateQueryData;
    -    property public final String id;
    -    property public final String type;
    -  }
    -
    -  public final class BeginGetCredentialRequest {
    -    ctor public BeginGetCredentialRequest(java.util.List beginGetCredentialOptions);
    -    ctor public BeginGetCredentialRequest(java.util.List beginGetCredentialOptions, optional androidx.credentials.provider.CallingAppInfo? callingAppInfo);
    -    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
    -    method public static androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
    -    method public java.util.List getBeginGetCredentialOptions();
    -    method public androidx.credentials.provider.CallingAppInfo? getCallingAppInfo();
    -    property public final java.util.List beginGetCredentialOptions;
    -    property public final androidx.credentials.provider.CallingAppInfo? callingAppInfo;
    -    field public static final androidx.credentials.provider.BeginGetCredentialRequest.Companion Companion;
    -  }
    -
    -  public static final class BeginGetCredentialRequest.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialRequest request);
    -    method public androidx.credentials.provider.BeginGetCredentialRequest? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public final class BeginGetCredentialResponse {
    -    ctor public BeginGetCredentialResponse(optional java.util.List credentialEntries, optional java.util.List actions, optional java.util.List authenticationActions, optional androidx.credentials.provider.RemoteEntry? remoteEntry);
    -    method public static android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public static androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
    -    method public java.util.List getActions();
    -    method public java.util.List getAuthenticationActions();
    -    method public java.util.List getCredentialEntries();
    -    method public androidx.credentials.provider.RemoteEntry? getRemoteEntry();
    -    property public final java.util.List actions;
    -    property public final java.util.List authenticationActions;
    -    property public final java.util.List credentialEntries;
    -    property public final androidx.credentials.provider.RemoteEntry? remoteEntry;
    -    field public static final androidx.credentials.provider.BeginGetCredentialResponse.Companion Companion;
    -  }
    -
    -  public static final class BeginGetCredentialResponse.Builder {
    -    ctor public BeginGetCredentialResponse.Builder();
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAction(androidx.credentials.provider.Action action);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addAuthenticationAction(androidx.credentials.provider.AuthenticationAction authenticationAction);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder addCredentialEntry(androidx.credentials.provider.CredentialEntry entry);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse build();
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setActions(java.util.List actions);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setAuthenticationActions(java.util.List authenticationEntries);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setCredentialEntries(java.util.List entries);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse.Builder setRemoteEntry(androidx.credentials.provider.RemoteEntry? remoteEntry);
    -  }
    -
    -  public static final class BeginGetCredentialResponse.Companion {
    -    method public android.os.Bundle asBundle(androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public androidx.credentials.provider.BeginGetCredentialResponse? fromBundle(android.os.Bundle bundle);
    -  }
    -
    -  public class BeginGetCustomCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
    -    ctor public BeginGetCustomCredentialOption(String id, String type, android.os.Bundle candidateQueryData);
    -  }
    -
    -  public final class BeginGetPasswordOption extends androidx.credentials.provider.BeginGetCredentialOption {
    -    ctor public BeginGetPasswordOption(java.util.Set allowedUserIds, android.os.Bundle candidateQueryData, String id);
    -    method @VisibleForTesting public static androidx.credentials.provider.BeginGetPasswordOption createForTest(android.os.Bundle data, String id);
    -    method public java.util.Set getAllowedUserIds();
    -    property public final java.util.Set allowedUserIds;
    -  }
    -
    -  public final class BeginGetPublicKeyCredentialOption extends androidx.credentials.provider.BeginGetCredentialOption {
    -    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson);
    -    ctor public BeginGetPublicKeyCredentialOption(android.os.Bundle candidateQueryData, String id, String requestJson, optional byte[]? clientDataHash);
    -    method public byte[]? getClientDataHash();
    -    method public String getRequestJson();
    -    property public final byte[]? clientDataHash;
    -    property public final String requestJson;
    -  }
    -
    -  public final class CallingAppInfo {
    -    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo);
    -    ctor public CallingAppInfo(String packageName, android.content.pm.SigningInfo signingInfo, optional String? origin);
    -    method public String? getOrigin(String privilegedAllowlist);
    -    method public String getPackageName();
    -    method public android.content.pm.SigningInfo getSigningInfo();
    -    method public boolean isOriginPopulated();
    -    property public final String packageName;
    -    property public final android.content.pm.SigningInfo signingInfo;
    -  }
    -
    -  @RequiresApi(26) public final class CreateEntry {
    -    ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
    -    method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
    -    method public CharSequence getAccountName();
    -    method public CharSequence? getDescription();
    -    method public android.graphics.drawable.Icon? getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public Integer? getPasswordCredentialCount();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public Integer? getPublicKeyCredentialCount();
    -    method public Integer? getTotalCredentialCount();
    -    method public boolean isAutoSelectAllowed();
    -    property public final CharSequence accountName;
    -    property public final CharSequence? description;
    -    property public final android.graphics.drawable.Icon? icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    field public static final androidx.credentials.provider.CreateEntry.Companion Companion;
    -  }
    -
    -  public static final class CreateEntry.Builder {
    -    ctor public CreateEntry.Builder(CharSequence accountName, android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.CreateEntry build();
    -    method public androidx.credentials.provider.CreateEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.CreateEntry.Builder setDescription(CharSequence? description);
    -    method public androidx.credentials.provider.CreateEntry.Builder setIcon(android.graphics.drawable.Icon? icon);
    -    method public androidx.credentials.provider.CreateEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -    method public androidx.credentials.provider.CreateEntry.Builder setPasswordCredentialCount(int count);
    -    method public androidx.credentials.provider.CreateEntry.Builder setPublicKeyCredentialCount(int count);
    -    method public androidx.credentials.provider.CreateEntry.Builder setTotalCredentialCount(int count);
    -  }
    -
    -  public static final class CreateEntry.Companion {
    -    method public androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
    -  }
    -
    -  public abstract class CredentialEntry {
    -    method public static final androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public final CharSequence? getAffiliatedDomain();
    -    method public final androidx.credentials.provider.BeginGetCredentialOption getBeginGetCredentialOption();
    -    method public final CharSequence getEntryGroupId();
    -    method public final boolean isDefaultIconPreferredAsSingleProvider();
    -    property public final CharSequence? affiliatedDomain;
    -    property public final androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption;
    -    property public final CharSequence entryGroupId;
    -    property public final boolean isDefaultIconPreferredAsSingleProvider;
    -    field public static final androidx.credentials.provider.CredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class CredentialEntry.Companion {
    -    method public androidx.credentials.provider.CredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  @RequiresApi(34) public abstract class CredentialProviderService extends android.service.credentials.CredentialProviderService {
    -    ctor public CredentialProviderService();
    -    method public final void onBeginCreateCredential(android.service.credentials.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public abstract void onBeginCreateCredentialRequest(androidx.credentials.provider.BeginCreateCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public final void onBeginGetCredential(android.service.credentials.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public abstract void onBeginGetCredentialRequest(androidx.credentials.provider.BeginGetCredentialRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public final void onClearCredentialState(android.service.credentials.ClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -    method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
    -  }
    -
    -  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    -    ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
    -    ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
    -    method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public android.graphics.drawable.Icon getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence? getSubtitle();
    -    method public CharSequence getTitle();
    -    method public String getType();
    -    method public CharSequence? getTypeDisplayName();
    -    method public boolean hasDefaultIcon();
    -    method public boolean isAutoSelectAllowed();
    -    method public boolean isAutoSelectAllowedFromOption();
    -    property public final boolean hasDefaultIcon;
    -    property public final android.graphics.drawable.Icon icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isAutoSelectAllowedFromOption;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence? subtitle;
    -    property public final CharSequence title;
    -    property public String type;
    -    property public final CharSequence? typeDisplayName;
    -    field public static final androidx.credentials.provider.CustomCredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class CustomCredentialEntry.Builder {
    -    ctor public CustomCredentialEntry.Builder(android.content.Context context, String type, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption);
    -    method public androidx.credentials.provider.CustomCredentialEntry build();
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setEntryGroupId(CharSequence entryGroupId);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setSubtitle(CharSequence? subtitle);
    -    method public androidx.credentials.provider.CustomCredentialEntry.Builder setTypeDisplayName(CharSequence? typeDisplayName);
    -  }
    -
    -  public static final class CustomCredentialEntry.Companion {
    -    method public androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  public final class IntentHandlerConverters {
    -    method @RequiresApi(34) public static androidx.credentials.provider.BeginGetCredentialResponse? getBeginGetResponse(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.CreateCredentialResponse? getCreateCredentialCredentialResponse(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.CreateCredentialException? getCreateCredentialException(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.GetCredentialException? getGetCredentialException(android.content.Intent);
    -    method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
    -  }
    -
    -  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    -    ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
    -    ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
    -    method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public CharSequence? getDisplayName();
    -    method public android.graphics.drawable.Icon getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence getTypeDisplayName();
    -    method public CharSequence getUsername();
    -    method public boolean hasDefaultIcon();
    -    method public boolean isAutoSelectAllowed();
    -    method public boolean isAutoSelectAllowedFromOption();
    -    property public final CharSequence? displayName;
    -    property public final boolean hasDefaultIcon;
    -    property public final android.graphics.drawable.Icon icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isAutoSelectAllowedFromOption;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence typeDisplayName;
    -    property public final CharSequence username;
    -    field public static final androidx.credentials.provider.PasswordCredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class PasswordCredentialEntry.Builder {
    -    ctor public PasswordCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption);
    -    method public androidx.credentials.provider.PasswordCredentialEntry build();
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAffiliatedDomain(CharSequence? affiliatedDomain);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setDisplayName(CharSequence? displayName);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
    -    method public androidx.credentials.provider.PasswordCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -  }
    -
    -  public static final class PasswordCredentialEntry.Companion {
    -    method public androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  @RequiresApi(34) public final class PendingIntentHandler {
    -    ctor public PendingIntentHandler();
    -    method public static androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
    -    method public static androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
    -    method public static androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
    -    method public static void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public static void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
    -    method public static void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
    -    method public static void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
    -    method public static void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
    -    field public static final androidx.credentials.provider.PendingIntentHandler.Companion Companion;
    -  }
    -
    -  public static final class PendingIntentHandler.Companion {
    -    method public androidx.credentials.provider.BeginGetCredentialRequest? retrieveBeginGetCredentialRequest(android.content.Intent intent);
    -    method public androidx.credentials.provider.ProviderCreateCredentialRequest? retrieveProviderCreateCredentialRequest(android.content.Intent intent);
    -    method public androidx.credentials.provider.ProviderGetCredentialRequest? retrieveProviderGetCredentialRequest(android.content.Intent intent);
    -    method public void setBeginGetCredentialResponse(android.content.Intent intent, androidx.credentials.provider.BeginGetCredentialResponse response);
    -    method public void setCreateCredentialException(android.content.Intent intent, androidx.credentials.exceptions.CreateCredentialException exception);
    -    method public void setCreateCredentialResponse(android.content.Intent intent, androidx.credentials.CreateCredentialResponse response);
    -    method public void setGetCredentialException(android.content.Intent intent, androidx.credentials.exceptions.GetCredentialException exception);
    -    method public void setGetCredentialResponse(android.content.Intent intent, androidx.credentials.GetCredentialResponse response);
    -  }
    -
    -  public final class ProviderClearCredentialStateRequest {
    -    ctor public ProviderClearCredentialStateRequest(androidx.credentials.provider.CallingAppInfo callingAppInfo);
    -    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
    -    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
    -  }
    -
    -  public final class ProviderCreateCredentialRequest {
    -    ctor public ProviderCreateCredentialRequest(androidx.credentials.CreateCredentialRequest callingRequest, androidx.credentials.provider.CallingAppInfo callingAppInfo);
    -    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
    -    method public androidx.credentials.CreateCredentialRequest getCallingRequest();
    -    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
    -    property public final androidx.credentials.CreateCredentialRequest callingRequest;
    -  }
    -
    -  public final class ProviderGetCredentialRequest {
    -    ctor public ProviderGetCredentialRequest(java.util.List credentialOptions, androidx.credentials.provider.CallingAppInfo callingAppInfo);
    -    method public androidx.credentials.provider.CallingAppInfo getCallingAppInfo();
    -    method public java.util.List getCredentialOptions();
    -    property public final androidx.credentials.provider.CallingAppInfo callingAppInfo;
    -    property public final java.util.List credentialOptions;
    -  }
    -
    -  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    -    ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
    -    ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
    -    method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -    method public CharSequence? getDisplayName();
    -    method public android.graphics.drawable.Icon getIcon();
    -    method public java.time.Instant? getLastUsedTime();
    -    method public android.app.PendingIntent getPendingIntent();
    -    method public CharSequence getTypeDisplayName();
    -    method public CharSequence getUsername();
    -    method public boolean hasDefaultIcon();
    -    method public boolean isAutoSelectAllowed();
    -    method public boolean isAutoSelectAllowedFromOption();
    -    property public final CharSequence? displayName;
    -    property public final boolean hasDefaultIcon;
    -    property public final android.graphics.drawable.Icon icon;
    -    property public final boolean isAutoSelectAllowed;
    -    property public final boolean isAutoSelectAllowedFromOption;
    -    property public final java.time.Instant? lastUsedTime;
    -    property public final android.app.PendingIntent pendingIntent;
    -    property public final CharSequence typeDisplayName;
    -    property public final CharSequence username;
    -    field public static final androidx.credentials.provider.PublicKeyCredentialEntry.Companion Companion;
    -  }
    -
    -  public static final class PublicKeyCredentialEntry.Builder {
    -    ctor public PublicKeyCredentialEntry.Builder(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry build();
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setAutoSelectAllowed(boolean autoSelectAllowed);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDefaultIconPreferredAsSingleProvider(boolean isDefaultIconPreferredAsSingleProvider);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setDisplayName(CharSequence? displayName);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setIcon(android.graphics.drawable.Icon icon);
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry.Builder setLastUsedTime(java.time.Instant? lastUsedTime);
    -  }
    -
    -  public static final class PublicKeyCredentialEntry.Companion {
    -    method public androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    -  }
    -
    -  public final class RemoteEntry {
    -    ctor public RemoteEntry(android.app.PendingIntent pendingIntent);
    -    method public static androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
    -    method public android.app.PendingIntent getPendingIntent();
    -    property public final android.app.PendingIntent pendingIntent;
    -    field public static final androidx.credentials.provider.RemoteEntry.Companion Companion;
    -  }
    -
    -  public static final class RemoteEntry.Builder {
    -    ctor public RemoteEntry.Builder(android.app.PendingIntent pendingIntent);
    -    method public androidx.credentials.provider.RemoteEntry build();
    -  }
    -
    -  public static final class RemoteEntry.Companion {
    -    method public androidx.credentials.provider.RemoteEntry? fromRemoteEntry(android.service.credentials.RemoteEntry remoteEntry);
    -  }
    -
    -}
    -
    
    diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
    index a0d0eda..f899a45 100644
    --- a/credentials/credentials/api/restricted_current.txt
    +++ b/credentials/credentials/api/restricted_current.txt
    
    @@ -133,6 +133,11 @@
         method public void onResult(R result);
       }
     
    +  public final class CredentialManagerViewHandler {
    +    method public static androidx.credentials.PendingGetCredentialRequest? getPendingGetCredentialRequest(android.view.View);
    +    method public static void setPendingGetCredentialRequest(android.view.View, androidx.credentials.PendingGetCredentialRequest?);
    +  }
    +
       public abstract class CredentialOption {
         method public final java.util.Set getAllowedProviders();
         method public final android.os.Bundle getCandidateQueryData();
    @@ -245,6 +250,14 @@
       public static final class PasswordCredential.Companion {
       }
     
    +  public final class PendingGetCredentialRequest {
    +    ctor public PendingGetCredentialRequest(androidx.credentials.GetCredentialRequest request, kotlin.jvm.functions.Function1 callback);
    +    method public kotlin.jvm.functions.Function1 getCallback();
    +    method public androidx.credentials.GetCredentialRequest getRequest();
    +    property public final kotlin.jvm.functions.Function1 callback;
    +    property public final androidx.credentials.GetCredentialRequest request;
    +  }
    +
       @RequiresApi(34) public final class PrepareGetCredentialResponse {
         method public kotlin.jvm.functions.Function1? getCredentialTypeDelegate();
         method public kotlin.jvm.functions.Function0? getHasAuthResultsDelegate();
    @@ -740,7 +753,7 @@
         property public final android.content.pm.SigningInfo signingInfo;
       }
     
    -  @RequiresApi(26) public final class CreateEntry {
    +  @RequiresApi(23) public final class CreateEntry {
         ctor public CreateEntry(CharSequence accountName, android.app.PendingIntent pendingIntent, optional CharSequence? description, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon? icon, optional Integer? passwordCredentialCount, optional Integer? publicKeyCredentialCount, optional Integer? totalCredentialCount, optional boolean isAutoSelectAllowed);
         method public static androidx.credentials.provider.CreateEntry? fromCreateEntry(android.service.credentials.CreateEntry createEntry);
         method public CharSequence getAccountName();
    @@ -804,7 +817,7 @@
         method public abstract void onClearCredentialStateRequest(androidx.credentials.provider.ProviderClearCredentialStateRequest request, android.os.CancellationSignal cancellationSignal, android.os.OutcomeReceiver callback);
       }
     
    -  @RequiresApi(26) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    +  @RequiresApi(23) public final class CustomCredentialEntry extends androidx.credentials.provider.CredentialEntry {
         ctor @Deprecated public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
         ctor public CustomCredentialEntry(android.content.Context context, CharSequence title, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetCredentialOption beginGetCredentialOption, optional CharSequence? subtitle, optional CharSequence? typeDisplayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence entryGroupId, optional boolean isDefaultIconPreferredAsSingleProvider);
         method public static androidx.credentials.provider.CustomCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    @@ -855,7 +868,7 @@
         method @RequiresApi(34) public static android.credentials.GetCredentialResponse? getGetCredentialResponse(android.content.Intent);
       }
     
    -  @RequiresApi(26) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    +  @RequiresApi(23) public final class PasswordCredentialEntry extends androidx.credentials.provider.CredentialEntry {
         ctor @Deprecated public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
         ctor public PasswordCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPasswordOption beginGetPasswordOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional CharSequence? affiliatedDomain, optional boolean isDefaultIconPreferredAsSingleProvider);
         method public static androidx.credentials.provider.PasswordCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    @@ -941,7 +954,7 @@
         property public final java.util.List credentialOptions;
       }
     
    -  @RequiresApi(26) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
    +  @RequiresApi(23) public final class PublicKeyCredentialEntry extends androidx.credentials.provider.CredentialEntry {
         ctor @Deprecated public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed);
         ctor public PublicKeyCredentialEntry(android.content.Context context, CharSequence username, android.app.PendingIntent pendingIntent, androidx.credentials.provider.BeginGetPublicKeyCredentialOption beginGetPublicKeyCredentialOption, optional CharSequence? displayName, optional java.time.Instant? lastUsedTime, optional android.graphics.drawable.Icon icon, optional boolean isAutoSelectAllowed, optional boolean isDefaultIconPreferredAsSingleProvider);
         method public static androidx.credentials.provider.PublicKeyCredentialEntry? fromCredentialEntry(android.service.credentials.CredentialEntry credentialEntry);
    
    diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
    index da177eb..eef980c 100644
    --- a/credentials/credentials/build.gradle
    +++ b/credentials/credentials/build.gradle
    
    @@ -31,6 +31,7 @@
     
     dependencies {
         api("androidx.annotation:annotation:1.5.0")
    +    api("androidx.biometric:biometric:1.1.0")
         api(libs.kotlinStdlib)
         implementation(libs.kotlinCoroutinesCore)
     
    @@ -41,6 +42,7 @@
         androidTestImplementation(libs.testRunner)
         androidTestImplementation(libs.testRules)
         androidTestImplementation(libs.truth)
    +    androidTestImplementation(project(":core:core"))
         androidTestImplementation(project(":internal-testutils-truth"))
         androidTestImplementation(libs.kotlinCoroutinesAndroid)
         androidTestImplementation(project(":internal-testutils-runtime"), {
    @@ -49,6 +51,7 @@
     }
     
     android {
    +    compileSdkPreview "VanillaIceCream"
         namespace "androidx.credentials"
     
         defaultConfig {
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
    index 81f3196..390b9db 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoJavaTest.java
    
    @@ -116,7 +116,7 @@
             assertThat(displayInfo.getPreferDefaultProvider()).isEqualTo(expectedDefaultProvider);
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Test
         public void constructFromBundle_success() {
             String expectedUserId = "userId";
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
    index 14c0cc0..31c45f2 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateCredentialRequestDisplayInfoTest.kt
    
    @@ -107,7 +107,7 @@
             assertThat(displayInfo.preferDefaultProvider).isEqualTo(expectedDefaultProvider)
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Test
         fun constructFromBundle_success() {
             val expectedUserId = "userId"
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
    index 1e8d7ad..a8579df 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
    
    @@ -139,7 +139,7 @@
             assertThat(request.getPassword()).isEqualTo(passwordExpected);
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @SuppressWarnings("deprecation") // bundle.get(key)
         @Test
         public void getter_frameworkProperties() {
    @@ -191,7 +191,7 @@
             ).isEqualTo(R.drawable.ic_password);
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Test
         public void frameworkConversion_success() {
             String idExpected = "id";
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
    index f7405e2..977a984 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
    
    @@ -123,7 +123,7 @@
             assertThat(request.password).isEqualTo(passwordExpected)
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Suppress("DEPRECATION") // bundle.get(key)
         @Test
         fun getter_frameworkProperties() {
    @@ -187,7 +187,7 @@
             ).isEqualTo(R.drawable.ic_password)
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Test
         fun frameworkConversion_success() {
             val idExpected = "id"
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
    index cb095bb..893d0f9 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
    
    @@ -154,7 +154,7 @@
             assertThat(testJsonActual).isEqualTo(testJsonExpected);
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @SuppressWarnings("deprecation") // bundle.get(key)
         @Test
         public void getter_frameworkProperties_success() {
    @@ -210,7 +210,7 @@
             ).isEqualTo(R.drawable.ic_passkey);
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Test
         public void frameworkConversion_success() {
             byte[] clientDataHashExpected = "hash".getBytes();
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
    index cd3f948..d098456 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
    
    @@ -137,7 +137,7 @@
             assertThat(testJsonActual).isEqualTo(testJsonExpected)
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Suppress("DEPRECATION") // bundle.get(key)
         @Test
         fun getter_frameworkProperties_success() {
    @@ -206,7 +206,7 @@
             ).isEqualTo(R.drawable.ic_passkey)
         }
     
    -    @SdkSuppress(minSdkVersion = 28)
    +    @SdkSuppress(minSdkVersion = 34)
         @Test
         fun frameworkConversion_success() {
             val clientDataHashExpected = "hash".toByteArray()
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.java
    new file mode 100644
    index 0000000..06f71a5
    --- /dev/null
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerJavaTest.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 androidx.credentials;
    +
    +import static com.google.common.truth.Truth.assertThat;
    +
    +import static org.junit.Assert.assertNotNull;
    +
    +import android.content.Context;
    +import android.credentials.Credential;
    +import android.os.OutcomeReceiver;
    +import android.widget.EditText;
    +
    +import androidx.annotation.RequiresApi;
    +import androidx.credentials.internal.FrameworkImplHelper;
    +import androidx.test.core.app.ApplicationProvider;
    +import androidx.test.filters.SdkSuppress;
    +
    +import kotlin.Unit;
    +
    +import org.junit.Test;
    +
    +import java.util.Collections;
    +import java.util.concurrent.CountDownLatch;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.atomic.AtomicReference;
    +
    +@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
    +public class CredentialManagerViewHandlerJavaTest {
    +    private final Context mContext = ApplicationProvider.getApplicationContext();
    +
    +    private static final GetCredentialRequest GET_CRED_PASSWORD_REQ =
    +            new GetCredentialRequest.Builder()
    +                    .setCredentialOptions(Collections.singletonList(
    +                            new GetPasswordOption())).build();
    +    private static final android.credentials.GetCredentialRequest GET_CRED_PASSWORD_FRAMEWORK_REQ =
    +            FrameworkImplHelper.convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ);
    +
    +    @Test
    +    @RequiresApi(35)
    +    public void setPendingCredentialRequest_frameworkAttrSetSuccessfully() {
    +        EditText editText = new EditText(mContext);
    +
    +        PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(
    +                GET_CRED_PASSWORD_REQ,
    +                (response) -> Unit.INSTANCE);
    +
    +        CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,
    +                pendingGetCredentialRequest);
    +
    +        assertNotNull(editText.getPendingCredentialRequest());
    +        TestUtilsKt.equals(editText.getPendingCredentialRequest(),
    +                GET_CRED_PASSWORD_FRAMEWORK_REQ);
    +        assertThat(editText.getPendingCredentialCallback()).isInstanceOf(
    +                OutcomeReceiver.class
    +        );
    +    }
    +
    +    @Test
    +    @RequiresApi(35)
    +    public void setPendingCredentialRequest_callbackInvokedSuccessfully()
    +            throws InterruptedException {
    +        CountDownLatch latch1 = new CountDownLatch(1);
    +        AtomicReference getCredentialResponse = new AtomicReference<>();
    +        EditText editText = new EditText(mContext);
    +
    +        PendingGetCredentialRequest pendingGetCredentialRequest = new PendingGetCredentialRequest(
    +                GET_CRED_PASSWORD_REQ,
    +                (response) -> {
    +                    getCredentialResponse.set(response);
    +                    latch1.countDown();
    +                    return Unit.INSTANCE;
    +                });
    +
    +        CredentialManagerViewHandler.setPendingGetCredentialRequest(editText,
    +                pendingGetCredentialRequest);
    +
    +        assertNotNull(editText.getPendingCredentialRequest());
    +        TestUtilsKt.equals(editText.getPendingCredentialRequest(), GET_CRED_PASSWORD_FRAMEWORK_REQ);
    +        assertThat(editText.getPendingCredentialCallback()).isInstanceOf(
    +                OutcomeReceiver.class
    +        );
    +
    +        PasswordCredential passwordCredential = new PasswordCredential("id", "password");
    +        android.credentials.GetCredentialResponse frameworkPasswordResponse =
    +                new android.credentials.GetCredentialResponse(new Credential(
    +                        passwordCredential.getType(), passwordCredential.getData()));
    +        assertNotNull(editText.getPendingCredentialCallback());
    +        editText.getPendingCredentialCallback().onResult(frameworkPasswordResponse);
    +        latch1.await(50L, TimeUnit.MILLISECONDS);
    +
    +        assertThat(getCredentialResponse.get()).isNotNull();
    +        GetCredentialResponse expectedGetCredentialResponse = FrameworkImplHelper
    +                .convertGetResponseToJetpackClass(frameworkPasswordResponse);
    +        TestUtilsKt.equals(expectedGetCredentialResponse, getCredentialResponse.get());
    +    }
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt
    new file mode 100644
    index 0000000..9c762e4
    --- /dev/null
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerViewHandlerTest.kt
    
    @@ -0,0 +1,101 @@
    +/*
    + * 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 androidx.credentials
    +
    +import android.content.Context
    +import android.credentials.Credential
    +import android.os.OutcomeReceiver
    +import android.widget.EditText
    +import androidx.annotation.RequiresApi
    +import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetRequestToFrameworkClass
    +import androidx.credentials.internal.FrameworkImplHelper.Companion.convertGetResponseToJetpackClass
    +import androidx.test.core.app.ApplicationProvider
    +import androidx.test.filters.SdkSuppress
    +import com.google.common.truth.Truth.assertThat
    +import java.util.concurrent.CountDownLatch
    +import java.util.concurrent.TimeUnit
    +import java.util.concurrent.atomic.AtomicReference
    +import org.junit.Test
    +
    +@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
    +class CredentialManagerViewHandlerTest {
    +    private val mContext: Context = ApplicationProvider.getApplicationContext()
    +
    +    companion object {
    +        private val GET_CRED_PASSWORD_REQ = GetCredentialRequest.Builder()
    +            .setCredentialOptions(listOf(GetPasswordOption())).build()
    +        private val GET_CRED_PASSWORD_FRAMEWORK_REQ =
    +            convertGetRequestToFrameworkClass(GET_CRED_PASSWORD_REQ)
    +    }
    +
    +    @Test
    +    @RequiresApi(35)
    +    fun setPendingCredentialRequest_frameworkAttrSetSuccessfully() {
    +        val editText = EditText(mContext)
    +        val pendingGetCredentialRequest = PendingGetCredentialRequest(
    +            GET_CRED_PASSWORD_REQ
    +        ) { _: GetCredentialResponse? -> }
    +
    +        editText
    +            .pendingGetCredentialRequest = pendingGetCredentialRequest
    +
    +        equals(
    +            editText.pendingCredentialRequest!!,
    +            GET_CRED_PASSWORD_FRAMEWORK_REQ
    +        )
    +        assertThat(editText.pendingCredentialCallback).isInstanceOf(
    +            OutcomeReceiver::class.java
    +        )
    +    }
    +
    +    @Test
    +    @RequiresApi(35)
    +    @Throws(InterruptedException::class)
    +    fun setPendingCredentialRequest_callbackInvokedSuccessfully() {
    +        val latch1 = CountDownLatch(1)
    +        val getCredentialResponse = AtomicReference()
    +        val editText = EditText(mContext)
    +
    +        editText
    +            .pendingGetCredentialRequest = PendingGetCredentialRequest(
    +            GET_CRED_PASSWORD_REQ) {
    +                response ->
    +            getCredentialResponse.set(response)
    +            latch1.countDown()
    +        }
    +
    +        equals(editText.pendingCredentialRequest!!, GET_CRED_PASSWORD_FRAMEWORK_REQ)
    +        assertThat(editText.pendingCredentialCallback).isInstanceOf(
    +            OutcomeReceiver::class.java
    +        )
    +
    +        val passwordCredential = PasswordCredential("id", "password")
    +        val frameworkPasswordResponse =
    +            android.credentials.GetCredentialResponse(
    +                Credential(
    +                    passwordCredential.type, passwordCredential.data
    +                )
    +            )
    +
    +        editText.pendingCredentialCallback!!.onResult(frameworkPasswordResponse)
    +        latch1.await(50L, TimeUnit.MILLISECONDS)
    +
    +        assertThat(getCredentialResponse.get()).isNotNull()
    +        val expectedGetCredentialResponse =
    +            convertGetResponseToJetpackClass(frameworkPasswordResponse)
    +        equals(expectedGetCredentialResponse, getCredentialResponse.get())
    +    }
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java
    new file mode 100644
    index 0000000..3d43bfd6
    --- /dev/null
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestJavaTest.java
    
    @@ -0,0 +1,45 @@
    +/*
    + * 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 androidx.credentials;
    +
    +import static com.google.common.truth.Truth.assertThat;
    +
    +import androidx.annotation.RequiresApi;
    +import androidx.test.filters.SdkSuppress;
    +
    +import kotlin.Unit;
    +
    +import org.junit.Test;
    +
    +import java.util.Collections;
    +
    +@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
    +public class PendingGetCredentialRequestJavaTest {
    +    @Test
    +    @RequiresApi(35)
    +    public void constructor_setAndGetRequestThroughViewTag() {
    +        GetCredentialRequest request = new GetCredentialRequest.Builder()
    +                .setCredentialOptions(Collections.singletonList(new GetPasswordOption()))
    +                .build();
    +        PendingGetCredentialRequest pendingGetCredentialRequest =
    +                new PendingGetCredentialRequest(request,
    +                        (response) -> Unit.INSTANCE);
    +
    +        assertThat(pendingGetCredentialRequest.getRequest())
    +                .isSameInstanceAs(request);
    +    }
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt
    new file mode 100644
    index 0000000..bfeb30c
    --- /dev/null
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PendingGetCredentialRequestTest.kt
    
    @@ -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 androidx.credentials
    +
    +import androidx.annotation.RequiresApi
    +import androidx.test.filters.SdkSuppress
    +import com.google.common.truth.Truth.assertThat
    +import org.junit.Test
    +
    +@SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream")
    +class PendingGetCredentialRequestTest {
    +
    +    @Test
    +    @RequiresApi(35)
    +    fun constructor_setAndGetRequestThroughViewTag() {
    +        val request = GetCredentialRequest.Builder()
    +            .setCredentialOptions(listOf(GetPasswordOption()))
    +            .build()
    +
    +        val pendingGetCredentialRequest = PendingGetCredentialRequest(
    +            request
    +        ) { _: GetCredentialResponse? -> }
    +
    +        assertThat(pendingGetCredentialRequest.request)
    +            .isSameInstanceAs(request)
    +    }
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
    index a04f4a8..f841fef 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
    
    @@ -16,10 +16,16 @@
     
     package androidx.credentials
     
    +import android.content.pm.SigningInfo
     import android.graphics.drawable.Icon
     import android.os.Build
     import android.os.Bundle
    +import androidx.annotation.RequiresApi
     import androidx.credentials.provider.CallingAppInfo
    +import androidx.credentials.provider.ProviderCreateCredentialRequest
    +import androidx.credentials.provider.ProviderGetCredentialRequest
    +import com.google.common.truth.Truth.assertThat
    +import org.junit.Assert
     
     /** True if the two Bundles contain the same elements, and false otherwise. */
     @Suppress("DEPRECATION")
    @@ -83,3 +89,127 @@
     fun equals(a: CallingAppInfo, b: CallingAppInfo): Boolean {
         return a.packageName == b.packageName && a.origin == b.origin
     }
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +fun equals(
    +    createCredentialRequest: android.service.credentials.CreateCredentialRequest,
    +    request: ProviderCreateCredentialRequest
    +) {
    +    assertThat(createCredentialRequest.type).isEqualTo(
    +        request.callingRequest.type
    +    )
    +    equals(
    +        createCredentialRequest.data,
    +        request.callingRequest.credentialData
    +    )
    +    Assert.assertEquals(
    +        createCredentialRequest.callingAppInfo.packageName,
    +        request.callingAppInfo.packageName
    +    )
    +    Assert.assertEquals(
    +        createCredentialRequest.callingAppInfo.origin,
    +        request.callingAppInfo.origin
    +    )
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +fun equals(
    +    getCredentialRequest: android.service.credentials.GetCredentialRequest,
    +    request: ProviderGetCredentialRequest
    +) {
    +    Assert.assertEquals(
    +        getCredentialRequest.callingAppInfo.packageName,
    +        request.callingAppInfo.packageName
    +    )
    +    Assert.assertEquals(
    +        getCredentialRequest.callingAppInfo.origin,
    +        request.callingAppInfo.origin
    +    )
    +    equals(getCredentialRequest.credentialOptions, request.credentialOptions)
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +private fun equals(
    +    credentialOptions: List,
    +    credentialOptions1: List
    +) {
    +    assertThat(credentialOptions.size).isEqualTo(credentialOptions1.size)
    +    for (i in credentialOptions.indices) {
    +        equals(credentialOptions[i], credentialOptions1[i])
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +fun equals(
    +    frameworkRequest1: android.credentials.GetCredentialRequest,
    +    frameworkRequest2: android.credentials.GetCredentialRequest
    +) {
    +    equals(frameworkRequest1.data, frameworkRequest2.data)
    +    credentialOptionsEqual(frameworkRequest1.credentialOptions,
    +        frameworkRequest2.credentialOptions)
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +private fun credentialOptionsEqual(
    +    credentialOptions1: List,
    +    credentialOptions2: List
    +) {
    +    assertThat(credentialOptions1.size).isEqualTo(credentialOptions2.size)
    +    for (i in credentialOptions1.indices) {
    +        equals(credentialOptions1[i], credentialOptions2[i])
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +fun equals(
    +    credentialOption: android.credentials.CredentialOption,
    +    credentialOption1: CredentialOption
    +) {
    +    assertThat(credentialOption.type).isEqualTo(credentialOption1.type)
    +    assertThat(credentialOption.isSystemProviderRequired).isEqualTo(
    +        credentialOption1.isSystemProviderRequired
    +    )
    +    equals(credentialOption.credentialRetrievalData, credentialOption1.requestData)
    +    equals(credentialOption.candidateQueryData, credentialOption1.candidateQueryData)
    +    assertThat(credentialOption.allowedProviders).isEqualTo(credentialOption1.allowedProviders)
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +fun setUpCreatePasswordRequest(): android.service.credentials.CreateCredentialRequest {
    +    val passwordReq: CreateCredentialRequest = CreatePasswordRequest(
    +        "test-user-id", "test-password"
    +    )
    +    val request =
    +        android.service.credentials.CreateCredentialRequest(
    +            android.service.credentials.CallingAppInfo("calling_package", SigningInfo()),
    +            PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
    +            passwordReq.credentialData
    +        )
    +    return request
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +fun equals(
    +    credentialOption1: android.credentials.CredentialOption,
    +    credentialOption2: android.credentials.CredentialOption
    +) {
    +    equals(credentialOption1.candidateQueryData, credentialOption2.candidateQueryData)
    +    equals(credentialOption1.credentialRetrievalData, credentialOption2.credentialRetrievalData)
    +    assertThat(credentialOption1.type).isEqualTo(credentialOption2.type)
    +    assertThat(credentialOption1.allowedProviders).isEqualTo(credentialOption2.allowedProviders)
    +    assertThat(credentialOption1.isSystemProviderRequired).isEqualTo(
    +        credentialOption2.isSystemProviderRequired
    +    )
    +}
    +
    +fun equals(
    +    getCredentialResponse1: GetCredentialResponse,
    +    getCredentialResponse2: GetCredentialResponse
    +) {
    +    equals(getCredentialResponse1.credential, getCredentialResponse2.credential)
    +}
    +
    +fun equals(credential1: Credential, credential2: Credential) {
    +    assertThat(credential1.type).isEqualTo(credential2.type)
    +    equals(credential1.data, credential2.data)
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
    new file mode 100644
    index 0000000..9d44183
    --- /dev/null
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataJavaTest.java
    
    @@ -0,0 +1,209 @@
    +/*
    + * 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 androidx.credentials.provider;
    +
    +import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_ALLOWED_AUTHENTICATORS;
    +import static androidx.credentials.provider.BiometricPromptData.BUNDLE_HINT_CRYPTO_OP_ID;
    +
    +import static com.google.common.truth.Truth.assertThat;
    +
    +import static org.junit.Assert.assertThrows;
    +
    +import android.hardware.biometrics.BiometricManager;
    +import android.hardware.biometrics.BiometricPrompt;
    +import android.os.Bundle;
    +
    +import androidx.test.ext.junit.runners.AndroidJUnit4;
    +import androidx.test.filters.SdkSuppress;
    +import androidx.test.filters.SmallTest;
    +
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +
    +import javax.crypto.NullCipher;
    +
    +@RunWith(AndroidJUnit4.class)
    +@SmallTest
    +@SdkSuppress(minSdkVersion = 28)
    +public class BiometricPromptDataJavaTest {
    +
    +    private static final BiometricPrompt.CryptoObject TEST_CRYPTO_OBJECT = new
    +            BiometricPrompt.CryptoObject(new NullCipher());
    +
    +    private static final  int TEST_ALLOWED_AUTHENTICATOR = BiometricManager.Authenticators
    +            .BIOMETRIC_STRONG;
    +
    +    @Test
    +    public void construct_cryptoObjectStrongAllowedAuthenticator_success() {
    +        BiometricPromptData biometricPromptData = new BiometricPromptData(
    +                /*cryptoObject=*/TEST_CRYPTO_OBJECT,
    +                /*allowedAuthenticators=*/TEST_ALLOWED_AUTHENTICATOR
    +        );
    +
    +        assertThat(biometricPromptData.getAllowedAuthenticators())
    +                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
    +        assertThat(biometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);
    +    }
    +
    +    @Test
    +    public void construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {
    +        int expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK;
    +
    +        BiometricPromptData biometricPromptData = new BiometricPromptData.Builder().build();
    +
    +        assertThat(biometricPromptData.getCryptoObject()).isNull();
    +        assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(expectedAuthenticator);
    +    }
    +
    +    @Test
    +    public void construct_cryptoObjectExistsAuthenticatorNotProvided__defaultThrowsIAE() {
    +        assertThrows("Expected cryptoObject without strong authenticator to throw "
    +                        + "IllegalArgumentException",
    +                IllegalArgumentException.class,
    +                () -> new BiometricPromptData.Builder()
    +                        .setCryptoObject(TEST_CRYPTO_OBJECT).build()
    +        );
    +    }
    +
    +    @Test
    +    public void construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {
    +        BiometricPromptData biometricPromptData = new BiometricPromptData(
    +                /*cryptoObject=*/null,
    +                /*allowedAuthenticator=*/TEST_ALLOWED_AUTHENTICATOR
    +        );
    +
    +        assertThat(biometricPromptData.getCryptoObject()).isNull();
    +        assertThat(biometricPromptData.getAllowedAuthenticators()).isEqualTo(
    +                TEST_ALLOWED_AUTHENTICATOR);
    +    }
    +
    +    @Test
    +    public void construct_authenticatorNotAccepted_throwsIAE() {
    +        assertThrows("Expected invalid allowed authenticator to throw "
    +                        + "IllegalArgumentException",
    +                IllegalArgumentException.class,
    +                () -> new BiometricPromptData(
    +                        /*cryptoObject=*/null,
    +                        /*allowedAuthenticator=*/Integer.MIN_VALUE
    +                )
    +        );
    +    }
    +
    +    @Test
    +    public void build_requiredParamsOnly_success() {
    +        int expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;
    +
    +        BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder().build();
    +
    +        assertThat(actualBiometricPromptData.getAllowedAuthenticators()).isEqualTo(
    +                expectedAllowedAuthenticators);
    +        assertThat(actualBiometricPromptData.getCryptoObject()).isNull();
    +    }
    +
    +    @Test
    +    public void build_setCryptoObjectWithStrongAuthenticator_success() {
    +        BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()
    +                .setCryptoObject(TEST_CRYPTO_OBJECT)
    +                .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();
    +
    +        assertThat(actualBiometricPromptData.getCryptoObject()).isEqualTo(TEST_CRYPTO_OBJECT);
    +        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
    +                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
    +    }
    +
    +    @Test
    +    public void build_setAllowedAuthenticator_success() {
    +        BiometricPromptData actualBiometricPromptData = new BiometricPromptData.Builder()
    +                .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build();
    +
    +        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
    +                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
    +    }
    +
    +    // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available
    +    @Test
    +    public void fromBundle_validAllowedAuthenticator_success() {
    +        int expectedOpId = Integer.MIN_VALUE;
    +        Bundle inputBundle = new Bundle();
    +        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR);
    +        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
    +
    +        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
    +
    +        assertThat(actualBiometricPromptData).isNotNull();
    +        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
    +                .isEqualTo(TEST_ALLOWED_AUTHENTICATOR);
    +
    +    }
    +
    +    // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available
    +    @Test
    +    public void fromBundle_unrecognizedAllowedAuthenticator_success() {
    +        int expectedOpId = Integer.MIN_VALUE;
    +        Bundle inputBundle = new Bundle();
    +        int unrecognizedAuthenticator = Integer.MAX_VALUE;
    +        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator);
    +        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
    +
    +        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
    +
    +        assertThat(actualBiometricPromptData).isNotNull();
    +        assertThat(actualBiometricPromptData.getAllowedAuthenticators())
    +                .isEqualTo(unrecognizedAuthenticator);
    +
    +    }
    +
    +    // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available
    +    @Test
    +    public void fromBundle_invalidBundleKey_nullBiometricPromptData() {
    +        int expectedOpId = Integer.MIN_VALUE;
    +        Bundle inputBundle = new Bundle();
    +        int unrecognizedAuthenticator = Integer.MAX_VALUE;
    +        inputBundle.putInt("invalidKey", unrecognizedAuthenticator);
    +        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId);
    +
    +        BiometricPromptData actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle);
    +
    +        assertThat(actualBiometricPromptData).isNull();
    +    }
    +
    +    @Test
    +    public void toBundle_success() {
    +        BiometricPromptData testBiometricPromptData = new BiometricPromptData(TEST_CRYPTO_OBJECT,
    +                TEST_ALLOWED_AUTHENTICATOR);
    +        int expectedOpId = Integer.MIN_VALUE;
    +
    +        Bundle actualBundle = BiometricPromptData.toBundle(
    +                testBiometricPromptData);
    +
    +        assertThat(actualBundle).isNotNull();
    +        assertThat(actualBundle.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
    +                TEST_ALLOWED_AUTHENTICATOR
    +        );
    +        assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId);
    +    }
    +
    +    @Test
    +    public void build_setInvalidAllowedAuthenticator_throwsIAE() {
    +        assertThrows("Expected invalid allowed authenticator to throw "
    +                        + "IllegalArgumentException",
    +                IllegalArgumentException.class,
    +                () -> new BiometricPromptData.Builder().setAllowedAuthenticators(-10000).build()
    +        );
    +    }
    +
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
    new file mode 100644
    index 0000000..cc4ab5a
    --- /dev/null
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/BiometricPromptDataTest.kt
    
    @@ -0,0 +1,198 @@
    +/*
    + * 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 androidx.credentials.provider
    +
    +import android.hardware.biometrics.BiometricManager
    +import android.hardware.biometrics.BiometricPrompt
    +import android.os.Bundle
    +import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
    +import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_CRYPTO_OP_ID
    +import androidx.test.ext.junit.runners.AndroidJUnit4
    +import androidx.test.filters.SdkSuppress
    +import androidx.test.filters.SmallTest
    +import com.google.common.truth.Truth.assertThat
    +import javax.crypto.NullCipher
    +import org.junit.Assert.assertThrows
    +import org.junit.Test
    +import org.junit.runner.RunWith
    +
    +@RunWith(AndroidJUnit4::class)
    +@SdkSuppress(minSdkVersion = 28)
    +@SmallTest
    +class BiometricPromptDataTest {
    +    @Test
    +    fun construct_cryptoObjectStrongAllowedAuthenticator_success() {
    +        val biometricPromptData = BiometricPromptData(TEST_CRYPTO_OBJECT,
    +            TEST_ALLOWED_AUTHENTICATOR)
    +
    +        assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
    +        assertThat(biometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)
    +    }
    +
    +    @Test
    +    fun construct_cryptoObjectNullAuthenticatorNotProvided_successWithWeakAuthenticator() {
    +        val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_WEAK
    +
    +        val biometricPromptData = BiometricPromptData(
    +            null,
    +        )
    +
    +        assertThat(biometricPromptData.cryptoObject).isNull()
    +        assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)
    +    }
    +
    +    @Test
    +    fun construct_cryptoObjectExistsAuthenticatorNotProvided_defaultWeakAuthenticatorThrowsIAE() {
    +        assertThrows(
    +            "Expected invalid allowed authenticator with cryptoObject to throw " +
    +                "IllegalArgumentException",
    +            java.lang.IllegalArgumentException::class.java
    +        ) {
    +            BiometricPromptData(
    +                TEST_CRYPTO_OBJECT
    +            )
    +        }
    +    }
    +
    +    @Test
    +    fun construct_cryptoObjectNullAuthenticatorNonNull_successPassedInAuthenticator() {
    +        val expectedAuthenticator = BiometricManager.Authenticators.BIOMETRIC_STRONG
    +
    +        val biometricPromptData = BiometricPromptData(cryptoObject = null, expectedAuthenticator)
    +
    +        assertThat(biometricPromptData.cryptoObject).isNull()
    +        assertThat(biometricPromptData.allowedAuthenticators).isEqualTo(expectedAuthenticator)
    +    }
    +
    +    @Test
    +    fun construct_authenticatorNotAccepted_throwsIAE() {
    +        assertThrows(
    +            "Expected invalid allowed authenticator IllegalArgumentException",
    +            java.lang.IllegalArgumentException::class.java
    +        ) {
    +            BiometricPromptData(null, allowedAuthenticators = Int.MIN_VALUE)
    +        }
    +    }
    +
    +    @Test
    +    fun build_requiredParamsOnly_success() {
    +        val expectedAllowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
    +
    +        val actualBiometricPromptData = BiometricPromptData.Builder().build()
    +
    +        assertThat(actualBiometricPromptData.allowedAuthenticators)
    +            .isEqualTo(expectedAllowedAuthenticators)
    +        assertThat(actualBiometricPromptData.cryptoObject).isNull()
    +    }
    +
    +    @Test
    +    fun build_setCryptoObjectWithStrongAuthenticatorOnly_success() {
    +        val actualBiometricPromptData = BiometricPromptData.Builder()
    +            .setCryptoObject(TEST_CRYPTO_OBJECT)
    +            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG).build()
    +
    +        assertThat(actualBiometricPromptData.cryptoObject).isEqualTo(TEST_CRYPTO_OBJECT)
    +        assertThat(actualBiometricPromptData.allowedAuthenticators)
    +            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
    +    }
    +
    +    @Test
    +    fun build_setAllowedAuthenticator_success() {
    +        val actualBiometricPromptData = BiometricPromptData.Builder()
    +            .setAllowedAuthenticators(TEST_ALLOWED_AUTHENTICATOR).build()
    +
    +        assertThat(actualBiometricPromptData.allowedAuthenticators)
    +            .isEqualTo(TEST_ALLOWED_AUTHENTICATOR)
    +    }
    +
    +    @Test
    +    fun build_setInvalidAllowedAuthenticator_success() {
    +        assertThrows(
    +            "Expected builder invalid allowed authenticator to throw " +
    +                "IllegalArgumentException",
    +            java.lang.IllegalArgumentException::class.java
    +        ) {
    +            BiometricPromptData.Builder()
    +                .setAllowedAuthenticators(-10000).build()
    +        }
    +    }
    +
    +    // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available
    +    @Test
    +    fun fromBundle_validAllowedAuthenticator_success() {
    +        val expectedOpId = Integer.MIN_VALUE
    +        val inputBundle = Bundle()
    +        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, TEST_ALLOWED_AUTHENTICATOR)
    +        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
    +
    +        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
    +
    +        assertThat(actualBiometricPromptData).isNotNull()
    +        assertThat(actualBiometricPromptData!!.allowedAuthenticators).isEqualTo(
    +            TEST_ALLOWED_AUTHENTICATOR)
    +    }
    +
    +    // TODO(b/325469910) : Use the proper opId / CryptoObject structure when available
    +    @Test
    +    fun fromBundle_unrecognizedAllowedAuthenticator_success() {
    +        val expectedOpId = Integer.MIN_VALUE
    +        val inputBundle = Bundle()
    +        val unrecognizedAuthenticator = Integer.MAX_VALUE
    +        inputBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, unrecognizedAuthenticator)
    +        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
    +
    +        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
    +
    +        assertThat(actualBiometricPromptData).isNotNull()
    +        assertThat(actualBiometricPromptData!!.allowedAuthenticators).isEqualTo(
    +            unrecognizedAuthenticator)
    +    }
    +
    +    @Test
    +    fun fromBundle_invalidBundleKey_nullBiometricPromptData() {
    +        val expectedOpId = Integer.MIN_VALUE
    +        val inputBundle = Bundle()
    +        val unrecognizedAuthenticator = Integer.MAX_VALUE
    +        inputBundle.putInt("invalid key", unrecognizedAuthenticator)
    +        inputBundle.putInt(BUNDLE_HINT_CRYPTO_OP_ID, expectedOpId)
    +
    +        val actualBiometricPromptData = BiometricPromptData.fromBundle(inputBundle)
    +
    +        assertThat(actualBiometricPromptData).isNull()
    +    }
    +
    +    @Test
    +    fun toBundle_success() {
    +        val testBiometricPromptData = BiometricPromptData(TEST_CRYPTO_OBJECT,
    +            TEST_ALLOWED_AUTHENTICATOR)
    +        val expectedOpId = Integer.MIN_VALUE
    +
    +        val actualBundle = BiometricPromptData.toBundle(testBiometricPromptData)
    +
    +        assertThat(actualBundle).isNotNull()
    +        assertThat(actualBundle!!.getInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)).isEqualTo(
    +            TEST_ALLOWED_AUTHENTICATOR)
    +        assertThat(actualBundle.getInt(BUNDLE_HINT_CRYPTO_OP_ID)).isEqualTo(expectedOpId)
    +    }
    +
    +    private companion object {
    +        private val TEST_CRYPTO_OBJECT = BiometricPrompt.CryptoObject(NullCipher())
    +
    +        private const val TEST_ALLOWED_AUTHENTICATOR = BiometricManager
    +            .Authenticators.BIOMETRIC_STRONG
    +    }
    +}
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
    index cac92a9..d5b4549 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/CallingAppInfoTest.kt
    
    @@ -25,6 +25,7 @@
     import androidx.test.filters.SmallTest
     import com.google.common.truth.Truth.assertThat
     import org.junit.Assert.assertFalse
    +import org.junit.Assert.assertNotNull
     import org.junit.Assert.assertThrows
     import org.junit.Assert.assertTrue
     import org.junit.BeforeClass
    @@ -183,7 +184,8 @@
                     val packageInfo = context.packageManager.getPackageInfo(
                         packageName, PackageManager.GET_SIGNING_CERTIFICATES
                     )
    -                signingInfo = packageInfo.signingInfo
    +                assertNotNull(signingInfo)
    +                signingInfo = packageInfo.signingInfo!!
                 } catch (_: PackageManager.NameNotFoundException) {
                 }
             }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
    index 5b332ac..167aabf 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerJavaTest.java
    
    @@ -18,13 +18,21 @@
     
     import static com.google.common.truth.Truth.assertThat;
     
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotNull;
    +
     import android.content.Intent;
    +import android.content.pm.SigningInfo;
    +import android.credentials.CredentialOption;
     import android.os.Build;
    +import android.os.Bundle;
    +import android.service.credentials.CallingAppInfo;
     
     import androidx.annotation.RequiresApi;
     import androidx.credentials.CreatePasswordResponse;
     import androidx.credentials.GetCredentialResponse;
     import androidx.credentials.PasswordCredential;
    +import androidx.credentials.TestUtilsKt;
     import androidx.credentials.exceptions.CreateCredentialInterruptedException;
     import androidx.credentials.exceptions.GetCredentialInterruptedException;
     import androidx.test.ext.junit.runners.AndroidJUnit4;
    @@ -34,6 +42,9 @@
     import org.junit.Test;
     import org.junit.runner.RunWith;
     
    +import java.util.ArrayList;
    +import java.util.Collections;
    +
     @RequiresApi(34)
     @RunWith(AndroidJUnit4.class)
     @SmallTest
    @@ -41,6 +52,244 @@
     public class PendingIntentHandlerJavaTest {
         private static final Intent BLANK_INTENT = new Intent();
     
    +    private static final android.credentials.CredentialOption
    +            GET_CREDENTIAL_OPTION = new CredentialOption.Builder(
    +            "type", new Bundle(), new Bundle())
    +            .build();
    +
    +    private static final android.service.credentials.GetCredentialRequest
    +            GET_CREDENTIAL_REQUEST = new android.service.credentials.GetCredentialRequest(
    +                    new CallingAppInfo(
    +                            "package_name", new SigningInfo()), new ArrayList<>(
    +                                    Collections.singleton(GET_CREDENTIAL_OPTION)));
    +
    +    private static final int BIOMETRIC_AUTHENTICATOR_TYPE = 1;
    +
    +    private static final int BIOMETRIC_AUTHENTICATOR_ERROR_CODE = 5;
    +
    +    private static final String BIOMETRIC_AUTHENTICATOR_ERROR_MSG = "error";
    +
    +    @Test
    +    public void test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
    +        for (int jetpackResult :
    +                AuthenticationResult.Companion
    +                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
    +            BiometricPromptResult biometricPromptResult =
    +                    new BiometricPromptResult(new AuthenticationResult(jetpackResult));
    +            android.service.credentials.CreateCredentialRequest request =
    +                    TestUtilsKt.setUpCreatePasswordRequest();
    +            Intent intent = prepareIntentWithCreateRequest(
    +                    request,
    +                    biometricPromptResult);
    +
    +            ProviderCreateCredentialRequest retrievedRequest =
    +                    PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent);
    +
    +            assertNotNull(request);
    +            TestUtilsKt.equals(request, retrievedRequest);
    +            assertNotNull(biometricPromptResult.getAuthenticationResult());
    +            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
    +                    .getAuthenticationType(), jetpackResult);
    +        }
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
    +        for (int jetpackResult :
    +                AuthenticationResult.Companion
    +                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().values()) {
    +            BiometricPromptResult biometricPromptResult =
    +                    new BiometricPromptResult(new AuthenticationResult(jetpackResult));
    +            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
    +                    biometricPromptResult);
    +
    +            ProviderGetCredentialRequest retrievedRequest =
    +                    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent);
    +
    +            assertNotNull(retrievedRequest);
    +            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
    +            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
    +                    .getAuthenticationType(), jetpackResult);
    +        }
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_resultConverted() {
    +        for (int frameworkResult :
    +                AuthenticationResult.Companion
    +                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
    +            BiometricPromptResult biometricPromptResult =
    +                    new BiometricPromptResult(
    +                            AuthenticationResult.Companion.createFrom$credentials_debug(
    +                                    frameworkResult,
    +                                    /*isFrameworkBiometricPrompt=*/true
    +                            ));
    +            android.service.credentials.CreateCredentialRequest request =
    +                    TestUtilsKt.setUpCreatePasswordRequest();
    +            int expectedResult =
    +                    AuthenticationResult.Companion
    +                            .getBiometricFrameworkToJetpackResultMap$credentials_debug()
    +                            .get(frameworkResult);
    +            Intent intent = prepareIntentWithCreateRequest(
    +                    request,
    +                    biometricPromptResult);
    +
    +            ProviderCreateCredentialRequest retrievedRequest =
    +                    PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent);
    +
    +            assertNotNull(request);
    +            TestUtilsKt.equals(request, retrievedRequest);
    +            assertNotNull(biometricPromptResult.getAuthenticationResult());
    +            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
    +                    .getAuthenticationType(), expectedResult);
    +        }
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_resultConverted() {
    +        for (int frameworkResult :
    +                AuthenticationResult.Companion
    +                        .getBiometricFrameworkToJetpackResultMap$credentials_debug().keySet()) {
    +            BiometricPromptResult biometricPromptResult =
    +                    new BiometricPromptResult(
    +                            AuthenticationResult.Companion.createFrom$credentials_debug(
    +                                    frameworkResult,
    +                                    /*isFrameworkBiometricPrompt=*/true
    +                            ));
    +            int expectedResult =
    +                    AuthenticationResult.Companion
    +                            .getBiometricFrameworkToJetpackResultMap$credentials_debug()
    +                            .get(frameworkResult);
    +            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
    +                    biometricPromptResult);
    +
    +            ProviderGetCredentialRequest retrievedRequest =
    +                    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent);
    +
    +            assertNotNull(retrievedRequest);
    +            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
    +            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationResult()
    +                    .getAuthenticationType(), expectedResult);
    +        }
    +    }
    +
    +
    +    @Test
    +    public void test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
    +        for (int jetpackError :
    +                AuthenticationError.Companion
    +                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
    +            BiometricPromptResult biometricPromptResult =
    +                    new BiometricPromptResult(
    +                            new AuthenticationError(
    +                                    jetpackError,
    +                                    BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
    +            android.service.credentials.CreateCredentialRequest request =
    +                    TestUtilsKt.setUpCreatePasswordRequest();
    +            Intent intent = prepareIntentWithCreateRequest(
    +                    request, biometricPromptResult);
    +
    +            ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
    +                    .retrieveProviderCreateCredentialRequest(intent);
    +
    +            assertNotNull(retrievedRequest);
    +            TestUtilsKt.equals(request, retrievedRequest);
    +            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
    +            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationError()
    +                    .getErrorCode(), jetpackError);
    +        }
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
    +        for (int jetpackError :
    +                AuthenticationError.Companion
    +                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().values()) {
    +            BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
    +                    new AuthenticationError(
    +                            jetpackError,
    +                            BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
    +            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
    +                    biometricPromptResult);
    +
    +            ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
    +                    .retrieveProviderGetCredentialRequest(intent);
    +
    +            assertNotNull(retrievedRequest);
    +            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
    +            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
    +            assertEquals(
    +                    retrievedRequest.getBiometricPromptResult().getAuthenticationError()
    +                            .getErrorCode(), jetpackError);
    +        }
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_errorConverted() {
    +        for (int frameworkError :
    +                AuthenticationError.Companion
    +                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
    +            BiometricPromptResult biometricPromptResult =
    +                    new BiometricPromptResult(
    +                            AuthenticationError.Companion.createFrom$credentials_debug(
    +                                    frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
    +                                    /*isFrameworkBiometricPrompt=*/true
    +                            ));
    +            android.service.credentials.CreateCredentialRequest request =
    +                    TestUtilsKt.setUpCreatePasswordRequest();
    +            int expectedErrorCode =
    +                    AuthenticationError.Companion
    +                            .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
    +                            .get(frameworkError);
    +            Intent intent = prepareIntentWithCreateRequest(
    +                    request, biometricPromptResult);
    +
    +            ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
    +                    .retrieveProviderCreateCredentialRequest(intent);
    +
    +            assertNotNull(retrievedRequest);
    +            TestUtilsKt.equals(request, retrievedRequest);
    +            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
    +            assertEquals(retrievedRequest.getBiometricPromptResult().getAuthenticationError()
    +                    .getErrorCode(), expectedErrorCode);
    +        }
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedErr() {
    +        for (int frameworkError :
    +                AuthenticationError.Companion
    +                        .getBiometricFrameworkToJetpackErrorMap$credentials_debug().keySet()) {
    +            BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
    +                    AuthenticationError.Companion.createFrom$credentials_debug(
    +                            frameworkError, BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
    +                            /*isFrameworkBiometricPrompt=*/true
    +                    ));
    +            Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
    +                    biometricPromptResult);
    +            int expectedErrorCode =
    +                    AuthenticationError.Companion
    +                            .getBiometricFrameworkToJetpackErrorMap$credentials_debug()
    +                            .get(frameworkError);
    +
    +            ProviderGetCredentialRequest retrievedRequest = PendingIntentHandler
    +                    .retrieveProviderGetCredentialRequest(intent);
    +
    +            assertNotNull(retrievedRequest);
    +            TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, retrievedRequest);
    +            assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +            assertNotNull(retrievedRequest.getBiometricPromptResult().getAuthenticationError());
    +            assertEquals(
    +                    retrievedRequest.getBiometricPromptResult().getAuthenticationError()
    +                            .getErrorCode(), expectedErrorCode);
    +        }
    +    }
    +
         @Test
         public void test_setGetCreateCredentialException() {
             if (Build.VERSION.SDK_INT >= 34) {
    @@ -157,6 +406,114 @@
         }
     
         @Test
    +    public void test_retrieveProviderCreateCredReqWithSuccessfulBpAuth() {
    +        BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
    +                new AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE));
    +
    +        android.service.credentials.CreateCredentialRequest request =
    +                TestUtilsKt.setUpCreatePasswordRequest();
    +
    +        Intent intent = prepareIntentWithCreateRequest(request,
    +                biometricPromptResult);
    +
    +        ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
    +                .retrieveProviderCreateCredentialRequest(intent);
    +
    +        assertNotNull(retrievedRequest);
    +        TestUtilsKt.equals(request, retrievedRequest);
    +        assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderCreateCredReqWithFailureBpAuth() {
    +        BiometricPromptResult biometricPromptResult =
    +                new BiometricPromptResult(
    +                        new AuthenticationError(
    +                                BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
    +                                BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
    +        android.service.credentials.CreateCredentialRequest request =
    +                TestUtilsKt.setUpCreatePasswordRequest();
    +        Intent intent = prepareIntentWithCreateRequest(
    +                request, biometricPromptResult);
    +
    +        ProviderCreateCredentialRequest retrievedRequest = PendingIntentHandler
    +                .retrieveProviderCreateCredentialRequest(intent);
    +
    +        assertNotNull(retrievedRequest);
    +        TestUtilsKt.equals(request, retrievedRequest);
    +        assertEquals(biometricPromptResult, retrievedRequest.getBiometricPromptResult());
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderGetCredReqWithSuccessfulBpAuth() {
    +        BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
    +                new AuthenticationResult(
    +                BIOMETRIC_AUTHENTICATOR_TYPE));
    +        Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
    +                biometricPromptResult);
    +
    +        ProviderGetCredentialRequest request = PendingIntentHandler
    +                .retrieveProviderGetCredentialRequest(intent);
    +
    +        assertNotNull(request);
    +        TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, request);
    +        assertEquals(biometricPromptResult, request.getBiometricPromptResult());
    +    }
    +
    +    @Test
    +    public void test_retrieveProviderGetCredReqWithFailingBpAuth() {
    +        BiometricPromptResult biometricPromptResult = new BiometricPromptResult(
    +                new AuthenticationError(
    +                        BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
    +                        BIOMETRIC_AUTHENTICATOR_ERROR_MSG));
    +        Intent intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST,
    +                biometricPromptResult);
    +
    +        ProviderGetCredentialRequest request = PendingIntentHandler
    +                .retrieveProviderGetCredentialRequest(intent);
    +
    +        assertNotNull(request);
    +        TestUtilsKt.equals(GET_CREDENTIAL_REQUEST, request);
    +        assertEquals(biometricPromptResult, request.getBiometricPromptResult());
    +    }
    +
    +    private Intent prepareIntentWithGetRequest(
    +            android.service.credentials.GetCredentialRequest request,
    +            BiometricPromptResult biometricPromptResult
    +    ) {
    +        Intent intent = new Intent();
    +        intent.putExtra(CredentialProviderService
    +                        .EXTRA_GET_CREDENTIAL_REQUEST, request);
    +        prepareIntentWithBiometricResult(intent, biometricPromptResult);
    +        return intent;
    +    }
    +
    +    private Intent prepareIntentWithCreateRequest(
    +            android.service.credentials.CreateCredentialRequest request,
    +            BiometricPromptResult biometricPromptResult) {
    +        Intent intent = new Intent();
    +        intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
    +                request);
    +        prepareIntentWithBiometricResult(intent, biometricPromptResult);
    +        return intent;
    +    }
    +
    +    private void prepareIntentWithBiometricResult(Intent intent,
    +            BiometricPromptResult biometricPromptResult) {
    +        if (biometricPromptResult.isSuccessful()) {
    +            assertNotNull(biometricPromptResult.getAuthenticationResult());
    +            intent.putExtra(AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE,
    +                    biometricPromptResult.getAuthenticationResult().getAuthenticationType());
    +        } else {
    +            assertNotNull(biometricPromptResult.getAuthenticationError());
    +            intent.putExtra(AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR,
    +                    biometricPromptResult.getAuthenticationError().getErrorCode());
    +            intent.putExtra(AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE,
    +                    biometricPromptResult.getAuthenticationError().getErrorMsg());
    +        }
    +    }
    +
    +    @Test
         public void test_createCredentialCredentialResponse() {
             if (Build.VERSION.SDK_INT >= 34) {
                 return;
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
    index c9ed499..37e91c6 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/PendingIntentHandlerTest.kt
    
    @@ -16,17 +16,26 @@
     package androidx.credentials.provider
     
     import android.content.Intent
    +import android.content.pm.SigningInfo
    +import android.credentials.CredentialOption
     import android.os.Build
    +import android.os.Bundle
    +import android.service.credentials.CallingAppInfo
    +import android.service.credentials.CreateCredentialRequest
    +import android.service.credentials.GetCredentialRequest
     import androidx.annotation.RequiresApi
     import androidx.credentials.CreatePasswordResponse
     import androidx.credentials.GetCredentialResponse
     import androidx.credentials.PasswordCredential
    +import androidx.credentials.equals
     import androidx.credentials.exceptions.CreateCredentialInterruptedException
     import androidx.credentials.exceptions.GetCredentialInterruptedException
    +import androidx.credentials.setUpCreatePasswordRequest
     import androidx.test.ext.junit.runners.AndroidJUnit4
     import androidx.test.filters.SdkSuppress
     import androidx.test.filters.SmallTest
     import com.google.common.truth.Truth.assertThat
    +import org.junit.Assert
     import org.junit.Test
     import org.junit.runner.RunWith
     
    @@ -35,6 +44,219 @@
     @RequiresApi(34)
     @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
     class PendingIntentHandlerTest {
    +    companion object {
    +        private val GET_CREDENTIAL_OPTION = CredentialOption.Builder(
    +            "type", Bundle(), Bundle()
    +        ).build()
    +
    +        private val GET_CREDENTIAL_REQUEST = GetCredentialRequest(
    +            CallingAppInfo("package_name", SigningInfo()),
    +            ArrayList(setOf(GET_CREDENTIAL_OPTION))
    +        )
    +
    +        private const val BIOMETRIC_AUTHENTICATOR_TYPE = 1
    +
    +        private const val BIOMETRIC_AUTHENTICATOR_ERROR_CODE = 5
    +
    +        private const val BIOMETRIC_AUTHENTICATOR_ERROR_MSG = "error"
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderCreateCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
    +        for (jetpackResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.values) {
    +            val biometricPromptResult = BiometricPromptResult(
    +                AuthenticationResult(jetpackResult)
    +            )
    +            val request = setUpCreatePasswordRequest()
    +            val intent = prepareIntentWithCreateRequest(
    +                request,
    +                biometricPromptResult
    +            )
    +
    +            val retrievedRequest = PendingIntentHandler
    +                .retrieveProviderCreateCredentialRequest(intent)
    +
    +            Assert.assertNotNull(request)
    +            equals(request, retrievedRequest!!)
    +            Assert.assertNotNull(biometricPromptResult.authenticationResult)
    +            Assert.assertEquals(retrievedRequest.biometricPromptResult!!.authenticationResult!!
    +                .authenticationType, jetpackResult)
    +        }
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderGetCredReqWithSuccessBpAuthJetpack_retrieveJetpackResult() {
    +        for (jetpackResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.values) {
    +            val biometricPromptResult = BiometricPromptResult(
    +                AuthenticationResult(jetpackResult)
    +            )
    +            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
    +
    +            val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    +
    +            Assert.assertNotNull(request)
    +            equals(GET_CREDENTIAL_REQUEST, request!!)
    +            Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
    +            Assert.assertEquals(request.biometricPromptResult!!.authenticationResult!!
    +                .authenticationType, jetpackResult)
    +        }
    +    }
    +
    +    // While possible to test non-conversion logic, that would equate functionally to the normal
    +    // jetpack tests as there is no validation.
    +    @Test
    +    fun test_retrieveProviderCreateCredReqWithSuccessBpAuthFramework_correctlyConvertedResult() {
    +        for (frameworkResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.keys) {
    +            val biometricPromptResult = BiometricPromptResult(
    +                AuthenticationResult.createFrom(uiAuthenticationType = frameworkResult,
    +                    isFrameworkBiometricPrompt = true)
    +            )
    +            val request = setUpCreatePasswordRequest()
    +            val expectedResult = AuthenticationResult
    +                .biometricFrameworkToJetpackResultMap[frameworkResult]
    +            val intent = prepareIntentWithCreateRequest(
    +                request,
    +                biometricPromptResult
    +            )
    +
    +            val retrievedRequest = PendingIntentHandler
    +                .retrieveProviderCreateCredentialRequest(intent)
    +
    +            Assert.assertNotNull(request)
    +            equals(request, retrievedRequest!!)
    +            Assert.assertNotNull(biometricPromptResult.authenticationResult)
    +            Assert.assertEquals(retrievedRequest.biometricPromptResult!!.authenticationResult!!
    +                .authenticationType, expectedResult)
    +        }
    +    }
    +
    +    // While possible to test non-conversion logic, that would equate functionally to the normal
    +    // jetpack tests as there is no validation.
    +    @Test
    +    fun test_retrieveProviderGetCredReqWithSuccessBpAuthFramework_correctlyConvertedResult() {
    +        for (frameworkResult in AuthenticationResult.biometricFrameworkToJetpackResultMap.keys) {
    +            val biometricPromptResult = BiometricPromptResult(
    +                AuthenticationResult.createFrom(uiAuthenticationType = frameworkResult,
    +                    isFrameworkBiometricPrompt = true)
    +            )
    +            val expectedResult = AuthenticationResult
    +                .biometricFrameworkToJetpackResultMap[frameworkResult]
    +            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
    +
    +            val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    +
    +            Assert.assertNotNull(request)
    +            equals(GET_CREDENTIAL_REQUEST, request!!)
    +            Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
    +            Assert.assertEquals(request.biometricPromptResult!!.authenticationResult!!
    +                .authenticationType, expectedResult)
    +        }
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderCreateCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
    +        for (jetpackError in AuthenticationError.biometricFrameworkToJetpackErrorMap.values) {
    +            val biometricPromptResult =
    +                BiometricPromptResult(
    +                    AuthenticationError(
    +                        jetpackError,
    +                        BIOMETRIC_AUTHENTICATOR_ERROR_MSG
    +                    )
    +                )
    +            val request = setUpCreatePasswordRequest()
    +            val intent = prepareIntentWithCreateRequest(
    +                request, biometricPromptResult
    +            )
    +
    +            val retrievedRequest = PendingIntentHandler
    +                .retrieveProviderCreateCredentialRequest(intent)
    +
    +            Assert.assertNotNull(retrievedRequest)
    +            equals(request, retrievedRequest!!)
    +            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
    +            Assert.assertEquals(
    +                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
    +                jetpackError
    +            )
    +        }
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderGetCredReqWithFailureBpAuthJetpack_retrieveJetpackError() {
    +        for (jetpackError in AuthenticationError.biometricFrameworkToJetpackErrorMap.values) {
    +            val biometricPromptResult = BiometricPromptResult(
    +                AuthenticationError(
    +                    jetpackError,
    +                    BIOMETRIC_AUTHENTICATOR_ERROR_MSG
    +                )
    +            )
    +            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
    +
    +            val retrievedRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    +
    +            Assert.assertNotNull(retrievedRequest)
    +            equals(GET_CREDENTIAL_REQUEST, retrievedRequest!!)
    +            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
    +            Assert.assertEquals(
    +                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
    +                jetpackError
    +            )
    +        }
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderCreateCredReqWithFailureBpAuthFramework_correctlyConvertedError() {
    +        for (frameworkError in AuthenticationError.biometricFrameworkToJetpackErrorMap.keys) {
    +            val biometricPromptResult =
    +                BiometricPromptResult(
    +                    AuthenticationError.createFrom(uiErrorCode = frameworkError,
    +                        uiErrorMessage = BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
    +                        isFrameworkBiometricPrompt = true
    +                    )
    +                )
    +            val expectedErrorCode = AuthenticationError
    +                .biometricFrameworkToJetpackErrorMap[frameworkError]
    +            val request = setUpCreatePasswordRequest()
    +            val intent = prepareIntentWithCreateRequest(
    +                request, biometricPromptResult
    +            )
    +
    +            val retrievedRequest = PendingIntentHandler
    +                .retrieveProviderCreateCredentialRequest(intent)
    +
    +            Assert.assertNotNull(retrievedRequest)
    +            equals(request, retrievedRequest!!)
    +            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
    +            Assert.assertEquals(
    +                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
    +                expectedErrorCode)
    +        }
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderGetCredReqWithFailureBpAuthFramework_correctlyConvertedError() {
    +        for (frameworkError in AuthenticationError.biometricFrameworkToJetpackErrorMap.keys) {
    +            val biometricPromptResult = BiometricPromptResult(
    +                AuthenticationError.createFrom(uiErrorCode = frameworkError,
    +                    uiErrorMessage = BIOMETRIC_AUTHENTICATOR_ERROR_MSG,
    +                    isFrameworkBiometricPrompt = true
    +                )
    +            )
    +            val expectedErrorCode = AuthenticationError
    +                .biometricFrameworkToJetpackErrorMap[frameworkError]
    +            val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
    +
    +            val retrievedRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    +
    +            Assert.assertNotNull(retrievedRequest)
    +            equals(GET_CREDENTIAL_REQUEST, retrievedRequest!!)
    +            Assert.assertNotNull(retrievedRequest.biometricPromptResult!!.authenticationError)
    +            Assert.assertEquals(
    +                retrievedRequest.biometricPromptResult!!.authenticationError!!.errorCode,
    +                expectedErrorCode)
    +        }
    +    }
    +
         @Test
         fun test_createCredentialException() {
             if (Build.VERSION.SDK_INT >= 34) {
    @@ -51,7 +273,7 @@
             assertThat(finalException).isEqualTo(initialException)
         }
     
    -    @Test()
    +    @Test
         fun test_createCredentialException_throwsWhenEmptyIntent() {
             if (Build.VERSION.SDK_INT >= 34) {
                 return
    @@ -62,6 +284,126 @@
         }
     
         @Test
    +    fun test_retrieveProviderCreateCredReqWithSuccessfulBpAuth() {
    +        val biometricPromptResult = BiometricPromptResult(
    +            AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE)
    +        )
    +        val request = setUpCreatePasswordRequest()
    +        val intent = prepareIntentWithCreateRequest(
    +            request,
    +            biometricPromptResult
    +        )
    +
    +        val retrievedRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
    +
    +        Assert.assertNotNull(request)
    +        equals(request, retrievedRequest!!)
    +        Assert.assertNotNull(biometricPromptResult.authenticationResult)
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderCreateCredReqWithFailureBpAuth() {
    +        val biometricPromptResult =
    +            BiometricPromptResult(
    +                AuthenticationError(
    +                    BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
    +                    BIOMETRIC_AUTHENTICATOR_ERROR_MSG
    +                )
    +            )
    +        val request = setUpCreatePasswordRequest()
    +        val intent = prepareIntentWithCreateRequest(
    +            request, biometricPromptResult
    +        )
    +
    +        val retrievedRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
    +
    +        Assert.assertNotNull(retrievedRequest)
    +        equals(request, retrievedRequest!!)
    +        Assert.assertEquals(biometricPromptResult, retrievedRequest.biometricPromptResult)
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderGetCredReqWithSuccessfulBpAuth() {
    +        val biometricPromptResult = BiometricPromptResult(
    +            AuthenticationResult(BIOMETRIC_AUTHENTICATOR_TYPE)
    +        )
    +        val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
    +
    +        val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    +
    +        Assert.assertNotNull(request)
    +        equals(GET_CREDENTIAL_REQUEST, request!!)
    +        Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
    +    }
    +
    +    @Test
    +    fun test_retrieveProviderGetCredReqWithFailingBpAuth() {
    +        val biometricPromptResult = BiometricPromptResult(
    +            AuthenticationError(
    +                BIOMETRIC_AUTHENTICATOR_ERROR_CODE,
    +                BIOMETRIC_AUTHENTICATOR_ERROR_MSG
    +            )
    +        )
    +        val intent = prepareIntentWithGetRequest(GET_CREDENTIAL_REQUEST, biometricPromptResult)
    +
    +        val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    +
    +        Assert.assertNotNull(request)
    +        equals(GET_CREDENTIAL_REQUEST, request!!)
    +        Assert.assertEquals(biometricPromptResult, request.biometricPromptResult)
    +    }
    +
    +    private fun prepareIntentWithGetRequest(
    +        request: GetCredentialRequest,
    +        biometricPromptResult: BiometricPromptResult
    +    ): Intent {
    +        val intent = Intent()
    +        intent.putExtra(
    +            android.service.credentials.CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
    +            request
    +        )
    +        prepareIntentWithBiometricResult(intent, biometricPromptResult)
    +        return intent
    +    }
    +
    +    private fun prepareIntentWithCreateRequest(
    +        request: CreateCredentialRequest,
    +        biometricPromptResult: BiometricPromptResult
    +    ): Intent {
    +        val intent = Intent()
    +        intent.putExtra(
    +            android.service.credentials.CredentialProviderService
    +                .EXTRA_CREATE_CREDENTIAL_REQUEST,
    +            request
    +        )
    +        prepareIntentWithBiometricResult(intent, biometricPromptResult)
    +        return intent
    +    }
    +
    +    private fun prepareIntentWithBiometricResult(
    +        intent: Intent,
    +        biometricPromptResult: BiometricPromptResult
    +    ) {
    +        if (biometricPromptResult.isSuccessful) {
    +            Assert.assertNotNull(biometricPromptResult.authenticationResult)
    +            intent.putExtra(
    +                AuthenticationResult.EXTRA_BIOMETRIC_AUTH_RESULT_TYPE,
    +                biometricPromptResult.authenticationResult!!.authenticationType
    +            )
    +        } else {
    +            Assert.assertNotNull(biometricPromptResult.authenticationError)
    +            intent.putExtra(
    +                AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR,
    +                biometricPromptResult.authenticationError!!.errorCode
    +            )
    +            intent.putExtra(
    +                AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE,
    +                biometricPromptResult.authenticationError!!.errorMsg
    +            )
    +        }
    +    }
    +
    +    @Test
         fun test_credentialException() {
             if (Build.VERSION.SDK_INT >= 34) {
                 return
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
    index 3f86607..22d51e6 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/ActionJavaTest.java
    
    @@ -22,7 +22,6 @@
     import static org.junit.Assert.assertThrows;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     
    @@ -80,9 +79,10 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void fromSlice_success() {
             Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
    -        Slice slice = Action.toSlice(originalAction);
    +        android.app.slice.Slice slice = Action.toSlice(originalAction);
     
             Action fromSlice = Action.fromSlice(slice);
     
    @@ -94,9 +94,10 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 34)
    +    @SuppressWarnings("deprecation")
         public void fromAction_success() {
             Action originalAction = new Action(TITLE, mPendingIntent, SUBTITLE);
    -        Slice slice = Action.toSlice(originalAction);
    +        android.app.slice.Slice slice = Action.toSlice(originalAction);
     
             Action action = Action.fromAction(new android.service.credentials.Action(slice));
     
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
    index d628df5..d5a75c5 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/AuthenticationActionJavaTest.java
    
    @@ -22,7 +22,6 @@
     import static org.junit.Assert.assertThrows;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     
    @@ -75,9 +74,10 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void fromSlice_success() {
             AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
    -        Slice slice = AuthenticationAction.toSlice(originalAction);
    +        android.app.slice.Slice slice = AuthenticationAction.toSlice(originalAction);
     
             AuthenticationAction fromSlice = AuthenticationAction.fromSlice(slice);
     
    @@ -87,9 +87,10 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 34)
    +    @SuppressWarnings("deprecation")
         public void fromAction_success() {
             AuthenticationAction originalAction = new AuthenticationAction(TITLE, mPendingIntent);
    -        Slice slice = AuthenticationAction.toSlice(originalAction);
    +        android.app.slice.Slice slice = AuthenticationAction.toSlice(originalAction);
             assertNotNull(slice);
     
             AuthenticationAction action = AuthenticationAction.fromAction(
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
    index 0de5cc2..1e060b2 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryJavaTest.java
    
    @@ -23,12 +23,17 @@
     import static org.junit.Assert.assertThrows;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     import android.graphics.Bitmap;
     import android.graphics.drawable.Icon;
    +import android.hardware.biometrics.BiometricManager;
    +import android.hardware.biometrics.BiometricPrompt;
    +import android.os.Build;
     
    +import androidx.annotation.RequiresApi;
    +import androidx.core.os.BuildCompat;
    +import androidx.credentials.provider.BiometricPromptData;
     import androidx.credentials.provider.CreateEntry;
     import androidx.test.core.app.ApplicationProvider;
     import androidx.test.ext.junit.runners.AndroidJUnit4;
    @@ -40,8 +45,10 @@
     
     import java.time.Instant;
     
    +import javax.crypto.NullCipher;
    +
     @RunWith(AndroidJUnit4.class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     public class CreateEntryJavaTest {
         private static final CharSequence ACCOUNT_NAME = "account_name";
    @@ -73,7 +80,17 @@
         }
     
         @Test
    -    public void constructor_allParameters_success() {
    +    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
    +    public void constructor_allParametersAboveApiO_success() {
    +        CreateEntry entry = constructEntryWithAllParams();
    +
    +        assertNotNull(entry);
    +        assertEntryWithAllParams(entry);
    +    }
    +
    +    @Test
    +    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
    +    public void constructor_allParametersApiOAndBelow_success() {
             CreateEntry entry = constructEntryWithAllParams();
     
             assertNotNull(entry);
    @@ -128,9 +145,10 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 34)
    +    @SuppressWarnings("deprecation")
         public void fromCreateEntry_allParams_success() {
             CreateEntry originalEntry = constructEntryWithAllParams();
    -        Slice slice = CreateEntry.toSlice(originalEntry);
    +        android.app.slice.Slice slice = CreateEntry.toSlice(originalEntry);
             assertNotNull(slice);
     
             CreateEntry entry = CreateEntry.fromCreateEntry(
    @@ -147,18 +165,22 @@
         private void assertEntryWithRequiredParams(CreateEntry entry) {
             assertThat(ACCOUNT_NAME.equals(entry.getAccountName()));
             assertThat(mPendingIntent).isEqualTo(entry.getPendingIntent());
    +        assertThat(entry.getBiometricPromptData()).isNull();
         }
     
         private CreateEntry constructEntryWithAllParams() {
    -        return new CreateEntry.Builder(
    +        CreateEntry.Builder testBuilder = new CreateEntry.Builder(
                     ACCOUNT_NAME,
                     mPendingIntent)
                     .setIcon(ICON)
                     .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
                     .setPasswordCredentialCount(PASSWORD_COUNT)
                     .setPublicKeyCredentialCount(PUBLIC_KEY_CREDENTIAL_COUNT)
    -                .setTotalCredentialCount(TOTAL_COUNT)
    -                .build();
    +                .setTotalCredentialCount(TOTAL_COUNT);
    +        if (BuildCompat.isAtLeastV()) {
    +            testBuilder.setBiometricPromptData(testBiometricPromptData());
    +        }
    +        return testBuilder.build();
         }
     
         private void assertEntryWithAllParams(CreateEntry entry) {
    @@ -169,5 +191,25 @@
             assertThat(PASSWORD_COUNT).isEqualTo(entry.getPasswordCredentialCount());
             assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(entry.getPublicKeyCredentialCount());
             assertThat(TOTAL_COUNT).isEqualTo(entry.getTotalCredentialCount());
    +        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
    +            assertAboveApiV(entry);
    +        } else {
    +            assertThat(entry.getBiometricPromptData()).isNull();
    +        }
    +    }
    +
    +    private static void assertAboveApiV(CreateEntry entry) {
    +        if (BuildCompat.isAtLeastV()) {
    +            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
    +                    testBiometricPromptData().getAllowedAuthenticators());
    +        }
    +    }
    +
    +    @RequiresApi(35)
    +    private static BiometricPromptData testBiometricPromptData() {
    +        return new BiometricPromptData.Builder()
    +                .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build();
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
    index dd830b39..b8c9dde 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CreateEntryTest.kt
    
    @@ -20,6 +20,12 @@
     import android.content.Intent
     import android.graphics.Bitmap
     import android.graphics.drawable.Icon
    +import android.hardware.biometrics.BiometricManager
    +import android.hardware.biometrics.BiometricPrompt
    +import android.os.Build
    +import androidx.annotation.RequiresApi
    +import androidx.core.os.BuildCompat
    +import androidx.credentials.provider.BiometricPromptData
     import androidx.credentials.provider.CreateEntry
     import androidx.credentials.provider.CreateEntry.Companion.fromCreateEntry
     import androidx.credentials.provider.CreateEntry.Companion.fromSlice
    @@ -28,8 +34,9 @@
     import androidx.test.ext.junit.runners.AndroidJUnit4
     import androidx.test.filters.SdkSuppress
     import androidx.test.filters.SmallTest
    -import com.google.common.truth.Truth
    +import com.google.common.truth.Truth.assertThat
     import java.time.Instant
    +import javax.crypto.NullCipher
     import org.junit.Assert
     import org.junit.Assert.assertFalse
     import org.junit.Assert.assertNotNull
    @@ -38,7 +45,7 @@
     import org.junit.runner.RunWith
     
     @RunWith(AndroidJUnit4::class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     class CreateEntryTest {
         private val mContext = ApplicationProvider.getApplicationContext()
    @@ -76,7 +83,8 @@
         }
     
         @Test
    -    fun constructor_allParameters_success() {
    +    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
    +    fun constructor_allParametersAboveApiO_success() {
             val entry = constructEntryWithAllParams()
     
             assertNotNull(entry)
    @@ -152,8 +160,9 @@
         }
     
         private fun assertEntryWithRequiredParams(entry: CreateEntry) {
    -        Truth.assertThat(ACCOUNT_NAME == entry.accountName)
    -        Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
    +        assertThat(ACCOUNT_NAME == entry.accountName)
    +        assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
    +        assertThat(entry.biometricPromptData).isNull()
         }
     
         private fun constructEntryWithAllParams(): CreateEntry {
    @@ -166,31 +175,39 @@
                 PASSWORD_COUNT,
                 PUBLIC_KEY_CREDENTIAL_COUNT,
                 TOTAL_COUNT,
    -            AUTO_SELECT_BIT
    +            AUTO_SELECT_BIT,
    +            if (BuildCompat.isAtLeastV()) testBiometricPromptData() else null,
             )
         }
     
         private fun assertEntryWithAllParams(entry: CreateEntry) {
    -        Truth.assertThat(ACCOUNT_NAME).isEqualTo(
    +        assertThat(ACCOUNT_NAME).isEqualTo(
                 entry.accountName
             )
    -        Truth.assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
    -        Truth.assertThat(ICON).isEqualTo(
    +        assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
    +        assertThat(ICON).isEqualTo(
                 entry.icon
             )
    -        Truth.assertThat(LAST_USED_TIME).isEqualTo(
    +        assertThat(LAST_USED_TIME).isEqualTo(
                 entry.lastUsedTime?.toEpochMilli()
             )
    -        Truth.assertThat(PASSWORD_COUNT).isEqualTo(
    +        assertThat(PASSWORD_COUNT).isEqualTo(
                 entry.getPasswordCredentialCount()
             )
    -        Truth.assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(
    +        assertThat(PUBLIC_KEY_CREDENTIAL_COUNT).isEqualTo(
                 entry.getPublicKeyCredentialCount()
             )
    -        Truth.assertThat(TOTAL_COUNT).isEqualTo(
    +        assertThat(TOTAL_COUNT).isEqualTo(
                 entry.getTotalCredentialCount()
             )
    -        Truth.assertThat(AUTO_SELECT_BIT).isTrue()
    +        assertThat(AUTO_SELECT_BIT).isTrue()
    +        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
    +            assertThat(entry.biometricPromptData!!.allowedAuthenticators).isEqualTo(
    +                testBiometricPromptData().allowedAuthenticators
    +            )
    +        } else {
    +            assertThat(entry.biometricPromptData).isNull()
    +        }
         }
     
         companion object {
    @@ -206,5 +223,13 @@
                     100, 100, Bitmap.Config.ARGB_8888
                 )
             )
    +
    +        @RequiresApi(35)
    +        private fun testBiometricPromptData(): BiometricPromptData {
    +            return BiometricPromptData.Builder()
    +                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build()
    +        }
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
    index cd6ab67..379c67a 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryJavaTest.java
    
    @@ -25,18 +25,22 @@
     import static org.junit.Assert.assertTrue;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     import android.graphics.Bitmap;
     import android.graphics.drawable.Icon;
    +import android.hardware.biometrics.BiometricManager;
    +import android.hardware.biometrics.BiometricPrompt;
     import android.os.Bundle;
     import android.service.credentials.CredentialEntry;
     
    +import androidx.annotation.RequiresApi;
    +import androidx.core.os.BuildCompat;
     import androidx.credentials.R;
     import androidx.credentials.TestUtilsKt;
     import androidx.credentials.provider.BeginGetCredentialOption;
     import androidx.credentials.provider.BeginGetCustomCredentialOption;
    +import androidx.credentials.provider.BiometricPromptData;
     import androidx.credentials.provider.CustomCredentialEntry;
     import androidx.test.core.app.ApplicationProvider;
     import androidx.test.ext.junit.runners.AndroidJUnit4;
    @@ -47,8 +51,11 @@
     import org.junit.runner.RunWith;
     
     import java.time.Instant;
    +
    +import javax.crypto.NullCipher;
    +
     @RunWith(AndroidJUnit4.class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     public class CustomCredentialEntryJavaTest {
         private static final CharSequence TITLE = "title";
    @@ -149,6 +156,7 @@
         }
     
         @Test
    +    @SdkSuppress(minSdkVersion = 28)
         public void builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
             CustomCredentialEntry entry = constructEntryWithRequiredParams();
     
    @@ -180,9 +188,11 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void fromSlice_requiredParams_success() {
             CustomCredentialEntry originalEntry = constructEntryWithRequiredParams();
    -        Slice slice = CustomCredentialEntry.toSlice(originalEntry);
    +        android.app.slice.Slice slice = CustomCredentialEntry
    +                .toSlice(originalEntry);
             CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(
                     slice);
             assertNotNull(entry);
    @@ -190,18 +200,21 @@
         }
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void fromSlice_allParams_success() {
             CustomCredentialEntry originalEntry = constructEntryWithAllParams();
    -        Slice slice = CustomCredentialEntry.toSlice(originalEntry);
    +        android.app.slice.Slice slice = CustomCredentialEntry
    +                .toSlice(originalEntry);
             CustomCredentialEntry entry = CustomCredentialEntry.fromSlice(slice);
             assertNotNull(entry);
             assertEntryWithAllParamsFromSlice(entry);
         }
         @Test
         @SdkSuppress(minSdkVersion = 34)
    +    @SuppressWarnings("deprecation")
         public void fromCredentialEntry_allParams_success() {
             CustomCredentialEntry originalEntry = constructEntryWithAllParams();
    -        Slice slice = CustomCredentialEntry.toSlice(originalEntry);
    +        android.app.slice.Slice slice = CustomCredentialEntry.toSlice(originalEntry);
             assertNotNull(slice);
             CustomCredentialEntry entry = CustomCredentialEntry.fromCredentialEntry(
                     new CredentialEntry("id", slice));
    @@ -220,11 +233,12 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
             CustomCredentialEntry entry = new CustomCredentialEntry
                     .Builder(mContext, TYPE, TITLE, mPendingIntent, mBeginCredentialOption).build();
     
    -        Slice slice = CustomCredentialEntry.toSlice(entry);
    +        android.app.slice.Slice slice = CustomCredentialEntry.toSlice(entry);
     
             assertNotNull(slice);
     
    @@ -237,12 +251,13 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void isDefaultIcon_customIconSetFromSlice_returnsTrue() {
             CustomCredentialEntry entry = new CustomCredentialEntry
                     .Builder(mContext, TYPE, TITLE, mPendingIntent, mBeginCredentialOption)
                     .setIcon(ICON).build();
     
    -        Slice slice = CustomCredentialEntry.toSlice(entry);
    +        android.app.slice.Slice slice = CustomCredentialEntry.toSlice(entry);
     
             assertNotNull(slice);
     
    @@ -291,7 +306,7 @@
             ).build();
         }
         private CustomCredentialEntry constructEntryWithAllParams() {
    -        return new CustomCredentialEntry.Builder(
    +        CustomCredentialEntry.Builder testBuilder = new CustomCredentialEntry.Builder(
                     mContext,
                     TYPE,
                     TITLE,
    @@ -302,8 +317,12 @@
                     .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
                     .setTypeDisplayName(TYPE_DISPLAY_NAME)
                     .setEntryGroupId(ENTRY_GROUP_ID)
    -                .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
    -                .build();
    +                .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT);
    +
    +        if (BuildCompat.isAtLeastV()) {
    +            testBuilder.setBiometricPromptData(testBiometricPromptData());
    +        }
    +        return testBuilder.build();
         }
         private void assertEntryWithRequiredParams(CustomCredentialEntry entry) {
             assertThat(TITLE.equals(entry.getTitle()));
    @@ -313,6 +332,7 @@
             assertThat(entry.getEntryGroupId()).isEqualTo(TITLE);
             assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                     DEFAULT_SINGLE_PROVIDER_ICON_BIT);
    +        assertThat(entry.getBiometricPromptData()).isNull();
         }
         private void assertEntryWithRequiredParamsFromSlice(CustomCredentialEntry entry) {
             assertThat(TITLE.equals(entry.getTitle()));
    @@ -322,6 +342,7 @@
             assertThat(entry.getEntryGroupId()).isEqualTo(TITLE);
             assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                     DEFAULT_SINGLE_PROVIDER_ICON_BIT);
    +        assertThat(entry.getBiometricPromptData()).isNull();
         }
         private void assertEntryWithAllParams(CustomCredentialEntry entry) {
             assertThat(TITLE.equals(entry.getTitle()));
    @@ -338,6 +359,12 @@
             assertThat(entry.getEntryGroupId()).isEqualTo(ENTRY_GROUP_ID);
             assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                     SINGLE_PROVIDER_ICON_BIT);
    +        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
    +            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
    +                    testBiometricPromptData().getAllowedAuthenticators());
    +        } else {
    +            assertThat(entry.getBiometricPromptData()).isNull();
    +        }
         }
         private void assertEntryWithAllParamsFromSlice(CustomCredentialEntry entry) {
             assertThat(TITLE.equals(entry.getTitle()));
    @@ -353,5 +380,19 @@
             assertThat(entry.getEntryGroupId()).isEqualTo(ENTRY_GROUP_ID);
             assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                     SINGLE_PROVIDER_ICON_BIT);
    +        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
    +            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
    +                    testBiometricPromptData().getAllowedAuthenticators());
    +        } else {
    +            assertThat(entry.getBiometricPromptData()).isNull();
    +        }
    +    }
    +
    +    @RequiresApi(35)
    +    private static BiometricPromptData testBiometricPromptData() {
    +        return new BiometricPromptData.Builder()
    +            .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
    +            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +            .build();
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
    index 69680b7..3d6baba 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/CustomCredentialEntryTest.kt
    
    @@ -19,13 +19,18 @@
     import android.content.Intent
     import android.graphics.Bitmap
     import android.graphics.drawable.Icon
    +import android.hardware.biometrics.BiometricManager
    +import android.hardware.biometrics.BiometricPrompt
     import android.os.Bundle
     import android.service.credentials.CredentialEntry
    +import androidx.annotation.RequiresApi
    +import androidx.core.os.BuildCompat
     import androidx.credentials.CredentialOption
     import androidx.credentials.R
     import androidx.credentials.equals
     import androidx.credentials.provider.BeginGetCredentialOption
     import androidx.credentials.provider.BeginGetCustomCredentialOption
    +import androidx.credentials.provider.BiometricPromptData
     import androidx.credentials.provider.CustomCredentialEntry
     import androidx.credentials.provider.CustomCredentialEntry.Companion.fromCredentialEntry
     import androidx.credentials.provider.CustomCredentialEntry.Companion.fromSlice
    @@ -36,13 +41,14 @@
     import androidx.test.filters.SmallTest
     import com.google.common.truth.Truth.assertThat
     import java.time.Instant
    +import javax.crypto.NullCipher
     import org.junit.Assert
     import org.junit.Assert.assertNotNull
     import org.junit.Assert.assertThrows
     import org.junit.Test
     import org.junit.runner.RunWith
     @RunWith(AndroidJUnit4::class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     class CustomCredentialEntryTest {
         private val mContext = ApplicationProvider.getApplicationContext()
    @@ -50,7 +56,6 @@
         private val mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
             PendingIntent.FLAG_IMMUTABLE)
         @Test
    -    @SdkSuppress(minSdkVersion = 28)
         fun constructor_requiredParams_success() {
             val entry = constructEntryWithRequiredParams()
             assertNotNull(entry)
    @@ -68,6 +73,7 @@
             assertNotNull(entry)
             assertEntryWithAllParams(entry)
         }
    +
         @Test
         fun constructor_emptyTitle_throwsIAE() {
             assertThrows(
    @@ -95,7 +101,7 @@
             }
         }
         @Test
    -    @SdkSuppress(minSdkVersion = 23)
    +    @SdkSuppress(minSdkVersion = 28)
         fun constructor_nullIcon_defaultIconSet() {
             val entry = constructEntryWithRequiredParams()
             assertThat(
    @@ -105,6 +111,7 @@
                 )
             ).isTrue()
         }
    +
         @Test
         fun constructor_setPreferredDefaultIconBit_retrieveSetPreferredDefaultIconBit() {
             val expectedPreferredDefaultIconBit = SINGLE_PROVIDER_ICON_BIT
    @@ -173,6 +180,7 @@
         }
     
         @Test
    +    @SdkSuppress(minSdkVersion = 28)
         fun builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
             val entry = CustomCredentialEntry.Builder(
                 mContext, TYPE, TITLE, mPendingIntent, BEGIN_OPTION).build()
    @@ -190,6 +198,7 @@
             assertThat(entry.entryGroupId).isEqualTo(TITLE)
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT)
    +        assertThat(entry.biometricPromptData).isNull()
         }
         @Test
         fun builder_setNonEmpyDeduplicationId_retrieveSetDeduplicationId() {
    @@ -327,19 +336,36 @@
             )
         }
         private fun constructEntryWithAllParams(): CustomCredentialEntry {
    -        return CustomCredentialEntry(
    -            mContext,
    -            TITLE,
    -            mPendingIntent,
    -            BEGIN_OPTION,
    -            SUBTITLE,
    -            TYPE_DISPLAY_NAME,
    -            Instant.ofEpochMilli(LAST_USED_TIME),
    -            ICON,
    -            IS_AUTO_SELECT_ALLOWED,
    -            ENTRY_GROUP_ID,
    -            SINGLE_PROVIDER_ICON_BIT
    -        )
    +        return if (BuildCompat.isAtLeastV()) {
    +            CustomCredentialEntry(
    +                mContext,
    +                TITLE,
    +                mPendingIntent,
    +                BEGIN_OPTION,
    +                SUBTITLE,
    +                TYPE_DISPLAY_NAME,
    +                Instant.ofEpochMilli(LAST_USED_TIME),
    +                ICON,
    +                IS_AUTO_SELECT_ALLOWED,
    +                ENTRY_GROUP_ID,
    +                SINGLE_PROVIDER_ICON_BIT,
    +                testBiometricPromptData()
    +            )
    +        } else {
    +            CustomCredentialEntry(
    +                mContext,
    +                TITLE,
    +                mPendingIntent,
    +                BEGIN_OPTION,
    +                SUBTITLE,
    +                TYPE_DISPLAY_NAME,
    +                Instant.ofEpochMilli(LAST_USED_TIME),
    +                ICON,
    +                IS_AUTO_SELECT_ALLOWED,
    +                ENTRY_GROUP_ID,
    +                SINGLE_PROVIDER_ICON_BIT
    +            )
    +        }
         }
         private fun assertEntryWithAllParams(entry: CustomCredentialEntry) {
             assertThat(TITLE == entry.title)
    @@ -352,6 +378,12 @@
             assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
             assertThat(ENTRY_GROUP_ID).isEqualTo(entry.entryGroupId)
    +        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
    +            assertThat(entry.biometricPromptData!!.allowedAuthenticators).isEqualTo(
    +                testBiometricPromptData().allowedAuthenticators)
    +        } else {
    +            assertThat(entry.biometricPromptData).isNull()
    +        }
         }
         private fun assertEntryWithAllParamsFromSlice(entry: CustomCredentialEntry) {
             assertThat(TITLE == entry.title)
    @@ -365,6 +397,12 @@
             assertThat(BEGIN_OPTION.type).isEqualTo(entry.type)
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
             assertThat(ENTRY_GROUP_ID).isEqualTo(entry.entryGroupId)
    +        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
    +            assertThat(entry.biometricPromptData!!.allowedAuthenticators).isEqualTo(
    +                testBiometricPromptData().allowedAuthenticators)
    +        } else {
    +            assertThat(entry.biometricPromptData).isNull()
    +        }
         }
         private fun assertEntryWithRequiredParams(entry: CustomCredentialEntry) {
             assertThat(TITLE == entry.title)
    @@ -374,6 +412,7 @@
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT)
             assertThat(entry.entryGroupId).isEqualTo(TITLE)
    +        assertThat(entry.biometricPromptData).isNull()
         }
         private fun assertEntryWithRequiredParamsFromSlice(entry: CustomCredentialEntry) {
             assertThat(TITLE == entry.title)
    @@ -382,6 +421,7 @@
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT)
             assertThat(entry.entryGroupId).isEqualTo(TITLE)
    +        assertThat(entry.biometricPromptData).isNull()
         }
         companion object {
             private val TITLE: CharSequence = "title"
    @@ -400,5 +440,13 @@
             private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
             private const val SINGLE_PROVIDER_ICON_BIT = true
             private const val ENTRY_GROUP_ID = "entryGroupId"
    +
    +        @RequiresApi(35)
    +        private fun testBiometricPromptData(): BiometricPromptData {
    +            return BiometricPromptData.Builder()
    +                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build()
    +        }
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
    index 639f2ad..10f4fbb 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryJavaTest.java
    
    @@ -25,18 +25,22 @@
     import static org.junit.Assert.assertTrue;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     import android.graphics.Bitmap;
     import android.graphics.drawable.Icon;
    +import android.hardware.biometrics.BiometricManager;
    +import android.hardware.biometrics.BiometricPrompt;
     import android.os.Bundle;
     import android.service.credentials.CredentialEntry;
     
    +import androidx.annotation.RequiresApi;
    +import androidx.core.os.BuildCompat;
     import androidx.credentials.PasswordCredential;
     import androidx.credentials.R;
     import androidx.credentials.TestUtilsKt;
     import androidx.credentials.provider.BeginGetPasswordOption;
    +import androidx.credentials.provider.BiometricPromptData;
     import androidx.credentials.provider.PasswordCredentialEntry;
     import androidx.test.core.app.ApplicationProvider;
     import androidx.test.ext.junit.runners.AndroidJUnit4;
    @@ -48,8 +52,11 @@
     
     import java.time.Instant;
     import java.util.HashSet;
    +
    +import javax.crypto.NullCipher;
    +
     @RunWith(AndroidJUnit4.class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     public class PasswordCredentialEntryJavaTest {
         private static final CharSequence USERNAME = "title";
    @@ -95,6 +102,7 @@
     
         @SdkSuppress(minSdkVersion = 28)
         @Test
    +    @SuppressWarnings("deprecation")
         public void isDefaultIcon_customIconSetFromSlice_returnsFalse() {
             PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                     mContext,
    @@ -103,7 +111,7 @@
                     mBeginGetPasswordOption
             ).setIcon(ICON).build();
     
    -        Slice slice = PasswordCredentialEntry.toSlice(entry);
    +        android.app.slice.Slice slice = PasswordCredentialEntry.toSlice(entry);
     
             assertNotNull(slice);
     
    @@ -117,6 +125,7 @@
     
         @SdkSuppress(minSdkVersion = 28)
         @Test
    +    @SuppressWarnings("deprecation")
         public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
             PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                     mContext,
    @@ -125,7 +134,7 @@
                     mBeginGetPasswordOption
             ).build();
     
    -        Slice slice = PasswordCredentialEntry.toSlice(entry);
    +        android.app.slice.Slice slice = PasswordCredentialEntry.toSlice(entry);
             assertNotNull(slice);
             PasswordCredentialEntry entryFromSlice = PasswordCredentialEntry
                     .fromSlice(slice);
    @@ -223,16 +232,20 @@
         @Test
         public void build_isAutoSelectAllowedDefault_false() {
             PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
    +
             assertFalse(entry.isAutoSelectAllowed());
         }
         @Test
         public void constructor_defaultAffiliatedDomain() {
             PasswordCredentialEntry entry = constructEntryWithRequiredParamsOnly();
    +
             assertThat(entry.getAffiliatedDomain()).isNull();
         }
    +
         @Test
         public void constructor_nonEmptyAffiliatedDomainSet_nonEmptyAffiliatedDomainRetrieved() {
             String expectedAffiliatedDomain = "non-empty";
    +
             PasswordCredentialEntry entryWithAffiliatedDomain = new PasswordCredentialEntry(
                     mContext,
                     USERNAME,
    @@ -245,39 +258,47 @@
                     expectedAffiliatedDomain,
                     false
             );
    +
             assertThat(entryWithAffiliatedDomain.getAffiliatedDomain())
                     .isEqualTo(expectedAffiliatedDomain);
         }
         @Test
    +    @SdkSuppress(minSdkVersion = 34)
         public void builder_constructDefault_containsOnlyDefaultValuesForSettableParameters() {
             PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(mContext, USERNAME,
                     mPendingIntent, mBeginGetPasswordOption).build();
    +
             assertThat(entry.getAffiliatedDomain()).isNull();
             assertThat(entry.getDisplayName()).isNull();
             assertThat(entry.getLastUsedTime()).isNull();
             assertThat(entry.isAutoSelectAllowed()).isFalse();
             assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
    +        assertThat(entry.getBiometricPromptData()).isNull();
         }
         @Test
         public void builder_setAffiliatedDomainNull_retrieveNullAffiliatedDomain() {
             PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(mContext, USERNAME,
                     mPendingIntent, mBeginGetPasswordOption).setAffiliatedDomain(null).build();
    +
             assertThat(entry.getAffiliatedDomain()).isNull();
         }
         @Test
         public void builder_setAffiliatedDomainNonNull_retrieveNonNullAffiliatedDomain() {
             String expectedAffiliatedDomain = "affiliated-domain";
    +
             PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                     mContext,
                     USERNAME,
                     mPendingIntent,
                     mBeginGetPasswordOption
             ).setAffiliatedDomain(expectedAffiliatedDomain).build();
    +
             assertThat(entry.getAffiliatedDomain()).isEqualTo(expectedAffiliatedDomain);
         }
         @Test
         public void builder_setPreferredDefaultIconBit_retrieveSetIconBit() {
             boolean expectedPreferredDefaultIconBit = SINGLE_PROVIDER_ICON_BIT;
    +
             PasswordCredentialEntry entry = new PasswordCredentialEntry.Builder(
                     mContext,
                     USERNAME,
    @@ -285,6 +306,7 @@
                     mBeginGetPasswordOption
             ).setDefaultIconPreferredAsSingleProvider(expectedPreferredDefaultIconBit)
                     .build();
    +
             assertThat(entry.isDefaultIconPreferredAsSingleProvider())
                     .isEqualTo(expectedPreferredDefaultIconBit);
         }
    @@ -323,20 +345,31 @@
                     mPendingIntent,
                     mBeginGetPasswordOption).build();
         }
    +
         private PasswordCredentialEntry constructEntryWithAllParams() {
    -        return new PasswordCredentialEntry.Builder(
    -                mContext,
    -                USERNAME,
    -                mPendingIntent,
    -                mBeginGetPasswordOption)
    -                .setDisplayName(DISPLAYNAME)
    -                .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
    -                .setIcon(ICON)
    -                .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
    -                .setAffiliatedDomain(AFFILIATED_DOMAIN)
    -                .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
    -                .build();
    +        if (BuildCompat.isAtLeastV()) {
    +            return new PasswordCredentialEntry.Builder(
    +                    mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption)
    +                    .setDisplayName(DISPLAYNAME)
    +                    .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
    +                    .setIcon(ICON)
    +                    .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
    +                    .setAffiliatedDomain(AFFILIATED_DOMAIN)
    +                    .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
    +                    .setBiometricPromptData(testBiometricPromptData()).build();
    +        } else {
    +            return new PasswordCredentialEntry.Builder(
    +                    mContext, USERNAME, mPendingIntent, mBeginGetPasswordOption)
    +                    .setDisplayName(DISPLAYNAME)
    +                    .setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME))
    +                    .setIcon(ICON)
    +                    .setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED)
    +                    .setAffiliatedDomain(AFFILIATED_DOMAIN)
    +                    .setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT)
    +                    .build();
    +        }
         }
    +
         private void assertEntryWithRequiredParamsOnly(PasswordCredentialEntry entry,
                 Boolean assertOptionIdOnly) {
             assertThat(USERNAME.equals(entry.getUsername()));
    @@ -346,6 +379,7 @@
             assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                     DEFAULT_SINGLE_PROVIDER_ICON_BIT);
             assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
    +        assertThat(entry.getBiometricPromptData()).isNull();
         }
         private void assertEntryWithAllParams(PasswordCredentialEntry entry) {
             assertThat(USERNAME.equals(entry.getUsername()));
    @@ -360,5 +394,19 @@
             assertThat(entry.isDefaultIconPreferredAsSingleProvider()).isEqualTo(
                     SINGLE_PROVIDER_ICON_BIT);
             assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
    +        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
    +            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
    +                    testBiometricPromptData().getAllowedAuthenticators());
    +        } else {
    +            assertThat(entry.getBiometricPromptData()).isNull();
    +        }
    +    }
    +
    +    @RequiresApi(35)
    +    private static BiometricPromptData testBiometricPromptData() {
    +        return new BiometricPromptData.Builder()
    +                .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build();
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
    index 53c9275..4794288 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PasswordCredentialEntryTest.kt
    
    @@ -19,13 +19,18 @@
     import android.content.Intent
     import android.graphics.Bitmap
     import android.graphics.drawable.Icon
    +import android.hardware.biometrics.BiometricManager
    +import android.hardware.biometrics.BiometricPrompt
     import android.os.Bundle
     import android.service.credentials.CredentialEntry
    +import androidx.annotation.RequiresApi
    +import androidx.core.os.BuildCompat
     import androidx.credentials.CredentialOption
     import androidx.credentials.PasswordCredential
     import androidx.credentials.R
     import androidx.credentials.equals
     import androidx.credentials.provider.BeginGetPasswordOption
    +import androidx.credentials.provider.BiometricPromptData
     import androidx.credentials.provider.PasswordCredentialEntry
     import androidx.credentials.provider.PasswordCredentialEntry.Companion.fromSlice
     import androidx.test.core.app.ApplicationProvider
    @@ -34,14 +39,16 @@
     import androidx.test.filters.SmallTest
     import com.google.common.truth.Truth.assertThat
     import java.time.Instant
    +import javax.crypto.NullCipher
     import junit.framework.TestCase.assertFalse
     import junit.framework.TestCase.assertNotNull
     import org.junit.Assert
     import org.junit.Assert.assertThrows
     import org.junit.Test
     import org.junit.runner.RunWith
    +
     @RunWith(AndroidJUnit4::class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     class PasswordCredentialEntryTest {
         private val mContext = ApplicationProvider.getApplicationContext()
    @@ -203,11 +210,21 @@
         @Test
         fun constructor_defaultAffiliatedDomain() {
             val defaultEntry = constructEntryWithRequiredParamsOnly()
    +
             assertThat(defaultEntry.affiliatedDomain).isNull()
         }
    +
    +    @Test
    +    fun constructor_defaultBiometricPromptData() {
    +        val defaultEntry = constructEntryWithRequiredParamsOnly()
    +
    +        assertThat(defaultEntry.biometricPromptData).isNull()
    +    }
    +
         @Test
         fun constructor_nonEmptyAffiliatedDomainSet_nonEmptyAffiliatedDomainRetrieved() {
             val expectedAffiliatedDomain = "non-empty"
    +
             val entryWithAffiliationType = PasswordCredentialEntry(
                 mContext,
                 USERNAME,
    @@ -218,6 +235,7 @@
                 ICON,
                 affiliatedDomain = expectedAffiliatedDomain
             )
    +
             assertThat(entryWithAffiliationType.affiliatedDomain).isEqualTo(expectedAffiliatedDomain)
         }
         @Test
    @@ -233,6 +251,7 @@
                 ICON,
                 isDefaultIconPreferredAsSingleProvider = expectedPreferredDefaultIconBit
             )
    +
             assertThat(entry.isDefaultIconPreferredAsSingleProvider)
                 .isEqualTo(expectedPreferredDefaultIconBit)
         }
    @@ -244,6 +263,7 @@
                 mPendingIntent,
                 BEGIN_OPTION
             )
    +
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT)
         }
    @@ -254,6 +274,7 @@
     
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
         }
    +
         @Test
         fun builder_constructDefault_containsOnlySetPropertiesAndDefaultValues() {
             val entry = PasswordCredentialEntry.Builder(
    @@ -276,6 +297,7 @@
             assertThat(entry.beginGetCredentialOption).isEqualTo(BEGIN_OPTION)
             assertThat(entry.affiliatedDomain).isNull()
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
    +        assertThat(entry.biometricPromptData).isNull()
         }
         @Test
         fun builder_setAffiliatedDomainNull_retrieveNullAffiliatedDomain() {
    @@ -285,17 +307,20 @@
                 mPendingIntent,
                 BEGIN_OPTION
             ).setAffiliatedDomain(null).build()
    +
             assertThat(entry.affiliatedDomain).isNull()
         }
         @Test
         fun builder_setAffiliatedDomainNonNull_retrieveNonNullAffiliatedDomain() {
             val expectedAffiliatedDomain = "name"
    +
             val entry = PasswordCredentialEntry.Builder(
                 mContext,
                 USERNAME,
                 mPendingIntent,
                 BEGIN_OPTION
             ).setAffiliatedDomain(expectedAffiliatedDomain).build()
    +
             assertThat(entry.affiliatedDomain).isEqualTo(expectedAffiliatedDomain)
         }
         @Test
    @@ -336,19 +361,36 @@
             )
         }
         private fun constructEntryWithAllParams(): PasswordCredentialEntry {
    -        return PasswordCredentialEntry(
    -            mContext,
    -            USERNAME,
    -            mPendingIntent,
    -            BEGIN_OPTION,
    -            DISPLAYNAME,
    -            LAST_USED_TIME,
    -            ICON,
    -            IS_AUTO_SELECT_ALLOWED,
    -            AFFILIATED_DOMAIN,
    -            SINGLE_PROVIDER_ICON_BIT
    -        )
    +        return if (BuildCompat.isAtLeastV()) {
    +            PasswordCredentialEntry(
    +                mContext,
    +                USERNAME,
    +                mPendingIntent,
    +                BEGIN_OPTION,
    +                DISPLAYNAME,
    +                LAST_USED_TIME,
    +                ICON,
    +                IS_AUTO_SELECT_ALLOWED,
    +                AFFILIATED_DOMAIN,
    +                SINGLE_PROVIDER_ICON_BIT,
    +                testBiometricPromptData(),
    +            )
    +        } else {
    +            PasswordCredentialEntry(
    +                mContext,
    +                USERNAME,
    +                mPendingIntent,
    +                BEGIN_OPTION,
    +                DISPLAYNAME,
    +                LAST_USED_TIME,
    +                ICON,
    +                IS_AUTO_SELECT_ALLOWED,
    +                AFFILIATED_DOMAIN,
    +                SINGLE_PROVIDER_ICON_BIT,
    +            )
    +        }
         }
    +
         private fun assertEntryWithRequiredParamsOnly(entry: PasswordCredentialEntry) {
             assertThat(USERNAME == entry.username)
             assertThat(mPendingIntent).isEqualTo(entry.pendingIntent)
    @@ -356,7 +398,9 @@
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT)
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
    +        assertThat(entry.biometricPromptData).isNull()
         }
    +
         private fun assertEntryWithAllParams(entry: PasswordCredentialEntry) {
             assertThat(USERNAME == entry.username)
             assertThat(DISPLAYNAME == entry.displayName)
    @@ -373,6 +417,13 @@
             assertThat(entry.affiliatedDomain).isEqualTo(AFFILIATED_DOMAIN)
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
    +        if (BuildCompat.isAtLeastV()) {
    +        // TODO(b/325469910) : Add cryptoObject tests once opId is retrievable
    +        assertThat(entry.biometricPromptData!!.allowedAuthenticators).isEqualTo(
    +            testBiometricPromptData().allowedAuthenticators)
    +        } else {
    +            assertThat(entry.biometricPromptData).isNull()
    +        }
         }
         companion object {
             private val USERNAME: CharSequence = "title"
    @@ -392,5 +443,12 @@
             private val AFFILIATED_DOMAIN = "affiliation-name"
             private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
             private const val SINGLE_PROVIDER_ICON_BIT = true
    +        @RequiresApi(35)
    +        private fun testBiometricPromptData(): BiometricPromptData {
    +            return BiometricPromptData.Builder()
    +                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build()
    +        }
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
    index 43cbd13..a7a6d76 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryJavaTest.java
    
    @@ -25,17 +25,21 @@
     import static org.junit.Assert.assertTrue;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     import android.graphics.Bitmap;
     import android.graphics.drawable.Icon;
    +import android.hardware.biometrics.BiometricManager;
    +import android.hardware.biometrics.BiometricPrompt;
     import android.os.Bundle;
     
    +import androidx.annotation.RequiresApi;
    +import androidx.core.os.BuildCompat;
     import androidx.credentials.PublicKeyCredential;
     import androidx.credentials.R;
     import androidx.credentials.TestUtilsKt;
     import androidx.credentials.provider.BeginGetPublicKeyCredentialOption;
    +import androidx.credentials.provider.BiometricPromptData;
     import androidx.credentials.provider.PublicKeyCredentialEntry;
     import androidx.test.core.app.ApplicationProvider;
     import androidx.test.ext.junit.runners.AndroidJUnit4;
    @@ -46,8 +50,11 @@
     import org.junit.runner.RunWith;
     
     import java.time.Instant;
    +
    +import javax.crypto.NullCipher;
    +
     @RunWith(AndroidJUnit4.class)
    -@SdkSuppress(minSdkVersion = 26)
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @SmallTest
     public class PublicKeyCredentialEntryJavaTest {
         private static final CharSequence USERNAME = "title";
    @@ -64,8 +71,10 @@
                         "{\"key1\":{\"key2\":{\"key3\":\"value3\"}}}");
         private final Context mContext = ApplicationProvider.getApplicationContext();
         private final Intent mIntent = new Intent();
    -    private final PendingIntent mPendingIntent = PendingIntent.getActivity(mContext, 0, mIntent,
    +    private final PendingIntent mPendingIntent = PendingIntent.getActivity(mContext, 0,
    +            mIntent,
                 PendingIntent.FLAG_IMMUTABLE);
    +
         @Test
         public void build_requiredParamsOnly_success() {
             PublicKeyCredentialEntry entry = constructWithRequiredParamsOnly();
    @@ -139,9 +148,10 @@
         }
         @Test
         @SdkSuppress(minSdkVersion = 34)
    +    @SuppressWarnings("deprecation")
         public void fromCredentialEntry_success() {
             PublicKeyCredentialEntry originalEntry = constructWithAllParams();
    -        Slice slice = PublicKeyCredentialEntry.toSlice(originalEntry);
    +        android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(originalEntry);
             assertNotNull(slice);
             PublicKeyCredentialEntry entry = PublicKeyCredentialEntry.fromCredentialEntry(
                     new android.service.credentials.CredentialEntry("id", slice));
    @@ -160,10 +170,11 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void isDefaultIcon_noIconSetFromSlice_returnsTrue() {
             PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
                     .Builder(mContext, USERNAME, mPendingIntent, mBeginOption).build();
    -        Slice slice = PublicKeyCredentialEntry.toSlice(entry);
    +        android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(entry);
     
             assertNotNull(slice);
     
    @@ -175,11 +186,12 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 28)
    +    @SuppressWarnings("deprecation")
         public void isDefaultIcon_customIconAfterSlice_returnsFalse() {
             PublicKeyCredentialEntry entry = new PublicKeyCredentialEntry
                     .Builder(mContext, USERNAME, mPendingIntent, mBeginOption)
                     .setIcon(ICON).build();
    -        Slice slice = PublicKeyCredentialEntry.toSlice(entry);
    +        android.app.slice.Slice slice = PublicKeyCredentialEntry.toSlice(entry);
     
             assertNotNull(slice);
     
    @@ -222,10 +234,15 @@
                     mBeginOption).build();
         }
         private PublicKeyCredentialEntry constructWithAllParams() {
    -        return new PublicKeyCredentialEntry.Builder(mContext, USERNAME, mPendingIntent,
    +        PublicKeyCredentialEntry.Builder testBuilder = new PublicKeyCredentialEntry
    +                .Builder(mContext, USERNAME, mPendingIntent,
                     mBeginOption).setAutoSelectAllowed(IS_AUTO_SELECT_ALLOWED).setDisplayName(
                     DISPLAYNAME).setLastUsedTime(Instant.ofEpochMilli(LAST_USED_TIME)).setIcon(
    -                ICON).setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT).build();
    +                ICON).setDefaultIconPreferredAsSingleProvider(SINGLE_PROVIDER_ICON_BIT);
    +        if (BuildCompat.isAtLeastV()) {
    +            testBuilder.setBiometricPromptData(testBiometricPromptData());
    +        }
    +        return testBuilder.build();
         }
         private void assertEntryWithRequiredParams(PublicKeyCredentialEntry entry) {
             assertThat(USERNAME.equals(entry.getUsername()));
    @@ -234,6 +251,7 @@
                     DEFAULT_SINGLE_PROVIDER_ICON_BIT);
             assertThat(entry.getAffiliatedDomain()).isNull();
             assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
    +        assertThat(entry.getBiometricPromptData()).isNull();
         }
         private void assertEntryWithAllParams(PublicKeyCredentialEntry entry) {
             assertThat(USERNAME.equals(entry.getUsername()));
    @@ -247,5 +265,19 @@
                     SINGLE_PROVIDER_ICON_BIT);
             assertThat(entry.getAffiliatedDomain()).isNull();
             assertThat(entry.getEntryGroupId()).isEqualTo(USERNAME);
    +        if (BuildCompat.isAtLeastV() && entry.getBiometricPromptData() != null) {
    +            assertThat(entry.getBiometricPromptData().getAllowedAuthenticators()).isEqualTo(
    +                    testBiometricPromptData().getAllowedAuthenticators());
    +        } else {
    +            assertThat(entry.getBiometricPromptData()).isNull();
    +        }
    +    }
    +
    +    @RequiresApi(35)
    +    private static BiometricPromptData testBiometricPromptData() {
    +        return new BiometricPromptData.Builder()
    +                .setCryptoObject(new BiometricPrompt.CryptoObject(new NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build();
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
    index 27b6c71..54ce2ee 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/PublicKeyCredentialEntryTest.kt
    
    @@ -19,13 +19,18 @@
     import android.content.Intent
     import android.graphics.Bitmap
     import android.graphics.drawable.Icon
    +import android.hardware.biometrics.BiometricManager
    +import android.hardware.biometrics.BiometricPrompt
     import android.os.Bundle
     import android.service.credentials.CredentialEntry
    +import androidx.annotation.RequiresApi
    +import androidx.core.os.BuildCompat
     import androidx.credentials.CredentialOption
     import androidx.credentials.PublicKeyCredential
     import androidx.credentials.R
     import androidx.credentials.equals
     import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
    +import androidx.credentials.provider.BiometricPromptData
     import androidx.credentials.provider.PublicKeyCredentialEntry
     import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromCredentialEntry
     import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.fromSlice
    @@ -36,12 +41,14 @@
     import androidx.test.filters.SmallTest
     import com.google.common.truth.Truth.assertThat
     import java.time.Instant
    +import javax.crypto.NullCipher
     import junit.framework.TestCase.assertNotNull
     import org.junit.Assert
     import org.junit.Assert.assertThrows
     import org.junit.Test
     import org.junit.runner.RunWith
    -@SdkSuppress(minSdkVersion = 26)
    +
    +@SdkSuppress(minSdkVersion = 26) // Instant usage
     @RunWith(AndroidJUnit4::class)
     @SmallTest
     class PublicKeyCredentialEntryTest {
    @@ -156,6 +163,7 @@
             assertThat(entry.beginGetCredentialOption).isEqualTo(BEGIN_OPTION)
             assertThat(entry.affiliatedDomain).isNull()
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
    +        assertThat(entry.biometricPromptData).isNull()
         }
     
         @Test
    @@ -278,17 +286,32 @@
             )
         }
         private fun constructWithAllParams(): PublicKeyCredentialEntry {
    -        return PublicKeyCredentialEntry(
    -            mContext,
    -            USERNAME,
    -            mPendingIntent,
    -            BEGIN_OPTION,
    -            DISPLAYNAME,
    -            Instant.ofEpochMilli(LAST_USED_TIME),
    -            ICON,
    -            IS_AUTO_SELECT_ALLOWED,
    -            SINGLE_PROVIDER_ICON_BIT
    -        )
    +        return if (BuildCompat.isAtLeastV()) {
    +            PublicKeyCredentialEntry(
    +                mContext,
    +                USERNAME,
    +                mPendingIntent,
    +                BEGIN_OPTION,
    +                DISPLAYNAME,
    +                Instant.ofEpochMilli(LAST_USED_TIME),
    +                ICON,
    +                IS_AUTO_SELECT_ALLOWED,
    +                SINGLE_PROVIDER_ICON_BIT,
    +                testBiometricPromptData()
    +            )
    +        } else {
    +            PublicKeyCredentialEntry(
    +                mContext,
    +                USERNAME,
    +                mPendingIntent,
    +                BEGIN_OPTION,
    +                DISPLAYNAME,
    +                Instant.ofEpochMilli(LAST_USED_TIME),
    +                ICON,
    +                IS_AUTO_SELECT_ALLOWED,
    +                SINGLE_PROVIDER_ICON_BIT,
    +            )
    +        }
         }
         private fun assertEntryWithRequiredParams(entry: PublicKeyCredentialEntry) {
             assertThat(USERNAME == entry.username)
    @@ -297,6 +320,7 @@
                 DEFAULT_SINGLE_PROVIDER_ICON_BIT)
             assertThat(entry.affiliatedDomain).isNull()
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
    +        assertThat(entry.biometricPromptData).isNull()
         }
         private fun assertEntryWithAllParams(entry: PublicKeyCredentialEntry) {
             assertThat(USERNAME == entry.username)
    @@ -309,6 +333,12 @@
             assertThat(entry.isDefaultIconPreferredAsSingleProvider).isEqualTo(SINGLE_PROVIDER_ICON_BIT)
             assertThat(entry.affiliatedDomain).isNull()
             assertThat(entry.entryGroupId).isEqualTo(USERNAME)
    +        if (BuildCompat.isAtLeastV() && entry.biometricPromptData != null) {
    +            assertThat(entry.biometricPromptData!!.allowedAuthenticators).isEqualTo(
    +                testBiometricPromptData().allowedAuthenticators)
    +        } else {
    +            assertThat(entry.biometricPromptData).isNull()
    +        }
         }
     
         companion object {
    @@ -329,5 +359,13 @@
             private const val IS_AUTO_SELECT_ALLOWED = true
             private const val DEFAULT_SINGLE_PROVIDER_ICON_BIT = false
             private const val SINGLE_PROVIDER_ICON_BIT = true
    +
    +        @RequiresApi(35)
    +        private fun testBiometricPromptData(): BiometricPromptData {
    +            return BiometricPromptData.Builder()
    +                .setCryptoObject(BiometricPrompt.CryptoObject(NullCipher()))
    +                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
    +                .build()
    +        }
         }
     }
    
    diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
    index 8a44a88..fe81ce4 100644
    --- a/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
    +++ b/credentials/credentials/src/androidTest/java/androidx/credentials/provider/ui/RemoteEntryJavaTest.java
    
    @@ -22,7 +22,6 @@
     import static org.junit.Assert.assertThrows;
     
     import android.app.PendingIntent;
    -import android.app.slice.Slice;
     import android.content.Context;
     import android.content.Intent;
     
    @@ -80,9 +79,10 @@
     
         @Test
         @SdkSuppress(minSdkVersion = 34)
    +    @SuppressWarnings("deprecation")
         public void fromRemoteEntry_success() {
             RemoteEntry originalEntry = new RemoteEntry(mPendingIntent);
    -        Slice slice = RemoteEntry.toSlice(originalEntry);
    +        android.app.slice.Slice slice = RemoteEntry.toSlice(originalEntry);
             assertNotNull(slice);
     
             RemoteEntry remoteEntry = RemoteEntry.fromRemoteEntry(
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt
    new file mode 100644
    index 0000000..b0028f2
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManagerViewHandler.kt
    
    @@ -0,0 +1,107 @@
    +/*
    + * 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.
    + */
    +
    +@file:JvmName("CredentialManagerViewHandler")
    +
    +package androidx.credentials
    +
    +import android.os.Build
    +import android.os.OutcomeReceiver
    +import android.util.Log
    +import android.view.View
    +import androidx.annotation.RequiresApi
    +import androidx.credentials.internal.FrameworkImplHelper
    +
    +/**
    + * An extension API to the [View] class that allows setting of a
    + * [PendingGetCredentialRequest], that in-turn contains a [GetCredentialRequest],
    + * and a callback to deliver the final [GetCredentialResponse]. The
    + * associated request is invoked when the [View] is focused/clicked by the user.
    + *
    + * A typical scenario for setting this request is a login screen with a username & password field.
    + * We recommend calling the [CredentialManager.getCredential] API when this login screen loads, so
    + * that the user can be presented with a selector with all credential options to choose from.
    + * In addition, we recommend using this API to set the same [GetCredentialRequest] that is passed
    + * to [CredentialManager.getCredential], on the username & password views. With that, if the
    + * user dismisses the initial selector, and then taps on either the username or the password
    + * field, they would see the same suggestions that they saw on the selector, but now on
    + * fallback UI experiences such as keyboard suggestions or drop-down lists,
    + * depending on the device capabilities.
    + *
    + * If you have multiple views on the screen that should invoke different requests as opposed to
    + * the same, you can simply use this API to set different requests on corresponding views, and
    + * hence a different set of suggestions will appear.
    + *
    + * Note that no errors are propagated to the [PendingGetCredentialRequest.callback].
    + * In a scenario where multiple suggestions are presented to the user as part of the
    + * keyboard suggestions for instance, it is possible that the user selects one, but the
    + * flow ends up in an error state, due to which the final [GetCredentialResponse] cannot be
    + * propagated. In that case, user will be taken back to the suggestions, and can very well
    + * select a different suggestion which would this time result in a success.
    + * The intermediate error states are not propagated to the developer, and only a final
    + * response, if any, is propagated.
    + *
    + * @property pendingGetCredentialRequest the [GetCredentialRequest] and
    + * the associated callback to be set on the view, and to be
    + * exercised when user focused on the view in question
    + */
    +
    +private const val TAG = "ViewHandler"
    +
    +@Suppress("NewApi")
    +var View.pendingGetCredentialRequest: PendingGetCredentialRequest?
    +    get() = getTag(R.id.androidx_credential_pendingCredentialRequest)
    +        as? PendingGetCredentialRequest
    +    set(value) {
    +        setTag(R.id.androidx_credential_pendingCredentialRequest, value)
    +        if (value != null) {
    +            if (Build.VERSION.SDK_INT >= 35 || (Build.VERSION.SDK_INT == 34 &&
    +                    Build.VERSION.PREVIEW_SDK_INT > 0)) {
    +                Api35Impl.setPendingGetCredentialRequest(this, value.request, value.callback)
    +            }
    +        } else {
    +            if (Build.VERSION.SDK_INT >= 35 || (Build.VERSION.SDK_INT == 34 &&
    +                    Build.VERSION.PREVIEW_SDK_INT > 0)) {
    +                Api35Impl.clearPendingGetCredentialRequest(this)
    +            }
    +        }
    +    }
    +
    +@RequiresApi(35)
    +private object Api35Impl {
    +    fun setPendingGetCredentialRequest(
    +        view: View,
    +        request: GetCredentialRequest,
    +        callback: (GetCredentialResponse) -> Unit,
    +    ) {
    +        val frameworkRequest = FrameworkImplHelper.convertGetRequestToFrameworkClass(request)
    +        val frameworkCallback = object : OutcomeReceiver<
    +            android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {
    +            override fun onResult(response: android.credentials.GetCredentialResponse) {
    +                callback.invoke(FrameworkImplHelper
    +                    .convertGetResponseToJetpackClass(response))
    +            }
    +            override fun onError(error: android.credentials.GetCredentialException) {
    +                Log.w(TAG, "Error: " + error.type + " , " + error.message)
    +            }
    +        }
    +        view.setPendingCredentialRequest(frameworkRequest, frameworkCallback)
    +    }
    +
    +    fun clearPendingGetCredentialRequest(view: View) {
    +        view.clearPendingCredentialRequest()
    +    }
    +}
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
    index a9a4c45d..ce0b2266 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
    
    @@ -147,7 +147,7 @@
     
             val classNames = mutableListOf()
             if (packageInfo.services != null) {
    -            for (serviceInfo in packageInfo.services) {
    +            for (serviceInfo in packageInfo.services!!) {
                     if (serviceInfo.metaData != null) {
                         val className = serviceInfo.metaData.getString(CREDENTIAL_PROVIDER_KEY)
                         if (className != null) {
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt
    new file mode 100644
    index 0000000..9cc49e7
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/PendingGetCredentialRequest.kt
    
    @@ -0,0 +1,38 @@
    +/*
    + * 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 androidx.credentials
    +
    +import android.view.View
    +
    +/**
    + * Request to be set on an Android [View], which will be invoked when the [View] is
    + * focused/clicked by the user.
    + *
    + * Note that the [callback] only handles a final [GetCredentialResponse] and no errors are
    + * propagated to the callback.
    + *
    + * See [View.setPendingCredentialRequest] for details on how this request will be used.
    + *
    + * @property request the [GetCredentialRequest] to be invoked when a given view on which this
    + * request is set is focused
    + * @property callback the callback on which the final [GetCredentialResponse] is returned, after
    + * the user has made its selections
    + */
    +class PendingGetCredentialRequest(
    +    val request: GetCredentialRequest,
    +    val callback: (GetCredentialResponse) -> Unit
    +)
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
    index 4377fab..1046485a 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/internal/FrameworkImplHelper.kt
    
    @@ -16,25 +16,30 @@
     
     package androidx.credentials.internal
     
    +import android.annotation.SuppressLint
     import android.content.Context
     import android.graphics.drawable.Icon
    +import android.os.Build
     import android.os.Bundle
     import androidx.annotation.RequiresApi
     import androidx.annotation.RestrictTo
    +import androidx.annotation.VisibleForTesting
     import androidx.credentials.CreateCredentialRequest
     import androidx.credentials.CreatePasswordRequest
     import androidx.credentials.CreatePublicKeyCredentialRequest
    +import androidx.credentials.Credential
    +import androidx.credentials.GetCredentialRequest
    +import androidx.credentials.GetCredentialResponse
     import androidx.credentials.R
     
    -@RequiresApi(23)
    -internal class FrameworkImplHelper {
    +@RequiresApi(34)
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +class FrameworkImplHelper {
         companion object {
             /**
              * Take the create request's `credentialData` and add SDK specific values to it.
              */
    -        @RestrictTo(RestrictTo.Scope.LIBRARY) // used from java tests
             @JvmStatic
    -        @RequiresApi(23)
             fun getFinalCreateCredentialData(
                 request: CreateCredentialRequest,
                 context: Context,
    @@ -58,5 +63,51 @@
                 )
                 return createCredentialData
             }
    +
    +        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        @JvmStatic
    +        fun convertGetResponseToJetpackClass(
    +            response: android.credentials.GetCredentialResponse
    +        ): GetCredentialResponse {
    +            val credential = response.credential
    +            return GetCredentialResponse(
    +                Credential.createFrom(
    +                    credential.type, credential.data
    +                )
    +            )
    +        }
    +
    +        @JvmStatic
    +        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        fun convertGetRequestToFrameworkClass(request: GetCredentialRequest):
    +            android.credentials.GetCredentialRequest {
    +            val builder = android.credentials.GetCredentialRequest.Builder(
    +                GetCredentialRequest.toRequestDataBundle(request)
    +            )
    +            request.credentialOptions.forEach {
    +                builder.addCredentialOption(
    +                    android.credentials.CredentialOption.Builder(
    +                        it.type, it.requestData, it.candidateQueryData
    +                    ).setIsSystemProviderRequired(
    +                        it.isSystemProviderRequired
    +                    ).setAllowedProviders(it.allowedProviders).build()
    +                )
    +            }
    +            setOriginForGetRequest(request, builder)
    +            return builder.build()
    +        }
    +
    +        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        @SuppressLint("MissingPermission")
    +        @VisibleForTesting
    +        @JvmStatic
    +        fun setOriginForGetRequest(
    +            request: GetCredentialRequest,
    +            builder: android.credentials.GetCredentialRequest.Builder
    +        ) {
    +            if (request.origin != null) {
    +                builder.setOrigin(request.origin)
    +            }
    +        }
         }
     }
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
    index 7be66882..3c07b5c 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
    
    @@ -13,6 +13,8 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
    +
     package androidx.credentials.provider
     
     import android.annotation.SuppressLint
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
    index 26c6b6b..2f02cc0 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
    
    @@ -13,6 +13,8 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
    +
     package androidx.credentials.provider
     
     import android.annotation.SuppressLint
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt
    new file mode 100644
    index 0000000..e73efb8
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationError.kt
    
    @@ -0,0 +1,147 @@
    +/*
    + * 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 androidx.credentials.provider
    +
    +import android.hardware.biometrics.BiometricPrompt
    +import android.util.Log
    +import androidx.annotation.RestrictTo
    +import java.util.Objects
    +import org.jetbrains.annotations.VisibleForTesting
    +
    +/**
    + * Error returned from the Biometric Prompt flow that is executed
    + * by [androidx.credentials.CredentialManager] after the user
    + * makes a selection on the Credential Manager account selector.
    + *
    + * @property errorCode the error code denoting what kind of error
    + * was encountered while the biometric prompt flow failed, must
    + * be one of the error codes defined in
    + * [androidx.biometric.BiometricPrompt] such as
    + * [androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE]
    + * or
    + * [androidx.biometric.BiometricPrompt.ERROR_TIMEOUT]
    + * @property errorMsg the message associated with the [errorCode] in the
    + * form that can be displayed on a UI.
    + *
    + * @see AuthenticationErrorTypes
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +class AuthenticationError @JvmOverloads constructor(
    +    val errorCode: @AuthenticationErrorTypes Int,
    +    val errorMsg: CharSequence? = null,
    +) {
    +
    +    companion object {
    +
    +        internal val TAG = "AuthenticationError"
    +
    +        @VisibleForTesting
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        const val EXTRA_BIOMETRIC_AUTH_ERROR = "BIOMETRIC_AUTH_ERROR"
    +        @VisibleForTesting
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        const val EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE = "EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE"
    +
    +        // The majority of this is unexpected to be sent, or the values are equal,
    +        // but should it arrive for any reason, is handled properly. This way
    +        // providers can be confident the Jetpack codes alone are enough.
    +        @VisibleForTesting
    +        internal val biometricFrameworkToJetpackErrorMap = linkedMapOf(
    +            BiometricPrompt.BIOMETRIC_ERROR_CANCELED to androidx.biometric.BiometricPrompt
    +                .ERROR_CANCELED,
    +            BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT to androidx.biometric.BiometricPrompt
    +                .ERROR_HW_NOT_PRESENT,
    +            BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE to androidx.biometric.BiometricPrompt
    +                .ERROR_HW_UNAVAILABLE,
    +            BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT to androidx.biometric.BiometricPrompt
    +                .ERROR_LOCKOUT,
    +            BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT to androidx.biometric.BiometricPrompt
    +                .ERROR_LOCKOUT_PERMANENT,
    +            BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS to androidx.biometric.BiometricPrompt
    +                .ERROR_NO_BIOMETRICS,
    +            BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL to androidx.biometric
    +                .BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,
    +            BiometricPrompt.BIOMETRIC_ERROR_NO_SPACE to androidx.biometric.BiometricPrompt
    +                .ERROR_NO_SPACE,
    +            BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED to androidx.biometric
    +                .BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED,
    +            BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT to androidx.biometric.BiometricPrompt
    +                .ERROR_TIMEOUT,
    +            BiometricPrompt.BIOMETRIC_ERROR_UNABLE_TO_PROCESS to androidx.biometric.BiometricPrompt
    +                .ERROR_UNABLE_TO_PROCESS,
    +            BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED to androidx.biometric.BiometricPrompt
    +                .ERROR_USER_CANCELED,
    +            BiometricPrompt.BIOMETRIC_ERROR_VENDOR to androidx.biometric.BiometricPrompt
    +                .ERROR_VENDOR
    +            // TODO(b/340334264) : Add NEGATIVE_BUTTON from FW once avail, or wrap this in
    +            // a credential manager specific error.
    +        )
    +
    +        internal fun convertFrameworkBiometricErrorToJetpack(frameworkCode: Int): Int {
    +            // Ignoring getOrDefault to allow this object down to API 21
    +            return if (biometricFrameworkToJetpackErrorMap.containsKey(frameworkCode)) {
    +                biometricFrameworkToJetpackErrorMap[frameworkCode]!!
    +            } else {
    +                Log.i(TAG, "Unexpected error code, $frameworkCode, ")
    +                frameworkCode
    +            }
    +        }
    +
    +        /**
    +         * Generates an instance of this class, to be called by an UI consumer that calls
    +         * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
    +         * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
    +         * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
    +         * [isFrameworkBiometricPrompt].
    +         *
    +         * @param uiErrorCode the error code used to create this error instance, typically using
    +         * the [androidx.biometric.BiometricPrompt]'s constants if conversion isn't desired, or
    +         * [android.hardware.biometrics.BiometricPrompt]'s constants if conversion *is* desired.
    +         * @param uiErrorMessage the message associated with the [uiErrorCode] in the
    +         * form that can be displayed on a UI.
    +         * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
    +         * requires conversion or not, set to true by default
    +         * @return an authentication error that has properly handled conversion of the err code
    +         */
    +        @JvmOverloads @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        internal fun createFrom(
    +            uiErrorCode: Int,
    +            uiErrorMessage: CharSequence,
    +            isFrameworkBiometricPrompt: Boolean = true,
    +        ): AuthenticationError =
    +            AuthenticationError(
    +                errorCode = if (isFrameworkBiometricPrompt)
    +                    convertFrameworkBiometricErrorToJetpack(uiErrorCode) else uiErrorCode,
    +                errorMsg = uiErrorMessage,
    +        )
    +    }
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) {
    +            return true
    +        }
    +        if (other is AuthenticationError) {
    +            return this.errorCode == other.errorCode &&
    +                this.errorMsg == other.errorMsg
    +        }
    +        return false
    +    }
    +
    +    override fun hashCode(): Int {
    +        return Objects.hash(this.errorCode, this.errorMsg)
    +    }
    +}
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt
    new file mode 100644
    index 0000000..567489a
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationErrorTypes.kt
    
    @@ -0,0 +1,60 @@
    +/*
    + * 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 androidx.credentials.provider
    +
    +import androidx.annotation.IntDef
    +import androidx.annotation.RestrictTo
    +import androidx.biometric.BiometricPrompt
    +import androidx.biometric.BiometricPrompt.ERROR_CANCELED
    +import androidx.biometric.BiometricPrompt.ERROR_HW_NOT_PRESENT
    +import androidx.biometric.BiometricPrompt.ERROR_HW_UNAVAILABLE
    +import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT
    +import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT
    +import androidx.biometric.BiometricPrompt.ERROR_NO_BIOMETRICS
    +import androidx.biometric.BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL
    +import androidx.biometric.BiometricPrompt.ERROR_NO_SPACE
    +import androidx.biometric.BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED
    +import androidx.biometric.BiometricPrompt.ERROR_TIMEOUT
    +import androidx.biometric.BiometricPrompt.ERROR_UNABLE_TO_PROCESS
    +import androidx.biometric.BiometricPrompt.ERROR_USER_CANCELED
    +import androidx.biometric.BiometricPrompt.ERROR_VENDOR
    +
    +/**
    + * This acts as a parameter hint for what [BiometricPrompt]'s error constants should be.
    + * You can learn more about the constants from [BiometricPrompt] to utilize best practices.
    + *
    + * @see BiometricPrompt
    + */
    +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
    +@Retention(AnnotationRetention.SOURCE)
    +@IntDef(value = [
    +    ERROR_CANCELED,
    +    ERROR_HW_NOT_PRESENT,
    +    ERROR_HW_UNAVAILABLE,
    +    ERROR_LOCKOUT,
    +    ERROR_LOCKOUT_PERMANENT,
    +    ERROR_NO_BIOMETRICS,
    +    ERROR_NO_DEVICE_CREDENTIAL,
    +    ERROR_NO_SPACE,
    +    ERROR_SECURITY_UPDATE_REQUIRED,
    +    ERROR_TIMEOUT,
    +    ERROR_UNABLE_TO_PROCESS,
    +    ERROR_USER_CANCELED,
    +    ERROR_VENDOR
    +])
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +annotation class AuthenticationErrorTypes
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt
    new file mode 100644
    index 0000000..79437d7
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationResult.kt
    
    @@ -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 androidx.credentials.provider
    +
    +import android.hardware.biometrics.BiometricPrompt
    +import android.util.Log
    +import androidx.annotation.RestrictTo
    +import androidx.credentials.provider.AuthenticationError.Companion.TAG
    +import java.util.Objects
    +import org.jetbrains.annotations.VisibleForTesting
    +
    +/**
    + * Successful result returned from the Biometric Prompt authentication
    + * flow handled by [androidx.credentials.CredentialManager].
    + *
    + * @property authenticationType the type of authentication (e.g. device credential or biometric)
    + * that was requested from and successfully provided by the user, corresponds to
    + * constants defined in [androidx.biometric.BiometricPrompt] such as
    + * [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC]
    + * or
    + * [androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL]
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +class AuthenticationResult(
    +    val authenticationType: @AuthenticatorResultTypes Int,
    +) {
    +
    +    companion object {
    +        @VisibleForTesting
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        const val EXTRA_BIOMETRIC_AUTH_RESULT_TYPE =
    +            "BIOMETRIC_AUTH_RESULT"
    +
    +        @VisibleForTesting
    +        internal val biometricFrameworkToJetpackResultMap = linkedMapOf(
    +            BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC to
    +                androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
    +            BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL to
    +                androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
    +            // TODO(b/340334264) : Add TYPE_UNKNOWN once avail from fw, though unexpected unless
    +            // very low API level, and may be ignored until jp only impl added in QPR, or other
    +            // ctr can be used directly once avail/ready
    +        )
    +
    +        internal fun convertFrameworkBiometricResultToJetpack(frameworkCode: Int): Int {
    +            // Ignoring getOrDefault to allow this object down to API 21
    +            return if (biometricFrameworkToJetpackResultMap.containsKey(frameworkCode)) {
    +                biometricFrameworkToJetpackResultMap[frameworkCode]!!
    +            } else {
    +                Log.i(TAG, "Non framework result code, $frameworkCode, ")
    +                frameworkCode
    +            }
    +        }
    +
    +        /**
    +         * Generates an instance of this class, to be called by an UI consumer that calls
    +         * [BiometricPrompt] API and needs the result to be wrapped by this class. The caller of
    +         * this API must specify whether the framework [android.hardware.biometrics.BiometricPrompt]
    +         * API or the jetpack [androidx.biometric.BiometricPrompt] API is used through
    +         * [isFrameworkBiometricPrompt].
    +         *
    +         * @param uiAuthenticationType the type of authentication (e.g. device credential or
    +         * biometric) that was requested from and successfully provided by the user, corresponds to
    +         * constants defined in [androidx.biometric.BiometricPrompt] if conversion is not desired,
    +         * or in [android.hardware.biometrics.BiometricPrompt] if conversion is desired
    +         * @param isFrameworkBiometricPrompt the bit indicating whether or not this error code
    +         * requires conversion or not, set to true by default
    +         * @return an authentication result that has properly handled conversion of the result types
    +         */
    +        @JvmOverloads @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        internal fun createFrom(
    +            uiAuthenticationType: Int,
    +            isFrameworkBiometricPrompt: Boolean = true,
    +        ): AuthenticationResult =
    +            AuthenticationResult(
    +                authenticationType = if (isFrameworkBiometricPrompt)
    +                    convertFrameworkBiometricResultToJetpack(uiAuthenticationType)
    +                else uiAuthenticationType
    +            )
    +    }
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) {
    +            return true
    +        }
    +        if (other is AuthenticationResult) {
    +            return this.authenticationType == other.authenticationType
    +        }
    +        return false
    +    }
    +
    +    override fun hashCode(): Int {
    +        return Objects.hash(this.authenticationType)
    +    }
    +}
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorResultTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorResultTypes.kt
    new file mode 100644
    index 0000000..4d37174
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorResultTypes.kt
    
    @@ -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 androidx.credentials.provider
    +
    +import androidx.annotation.IntDef
    +import androidx.annotation.RestrictTo
    +import androidx.biometric.BiometricPrompt
    +import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC
    +import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL
    +import androidx.biometric.BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN
    +
    +/**
    + * This acts as a parameter hint for what [BiometricPrompt]'s result constants should be.
    + * You can learn more about the constants from [BiometricPrompt] to utilize best practices.
    + *
    + * @see BiometricPrompt
    + */
    +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
    +@Retention(AnnotationRetention.SOURCE)
    +@IntDef(value = [
    +    AUTHENTICATION_RESULT_TYPE_BIOMETRIC,
    +    AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
    +    AUTHENTICATION_RESULT_TYPE_UNKNOWN
    +])
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +annotation class AuthenticatorResultTypes
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt
    new file mode 100644
    index 0000000..fe16da4
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticatorTypes.kt
    
    @@ -0,0 +1,37 @@
    +/*
    + * 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 androidx.credentials.provider
    +
    +import android.hardware.biometrics.BiometricManager
    +import androidx.annotation.IntDef
    +import androidx.annotation.RestrictTo
    +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
    +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
    +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
    +
    +/**
    + * This allows verification when users pass in [BiometricManager.Authenticators] constants; namely
    + * we can have a parameter hint that indicates what they should be. You can learn more about
    + * the constants from [BiometricManager.Authenticators] to utilize best practices.
    + *
    + * @see BiometricManager.Authenticators
    + */
    +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
    +@Retention(AnnotationRetention.SOURCE)
    +@IntDef(value = [BIOMETRIC_STRONG, BIOMETRIC_WEAK, DEVICE_CREDENTIAL])
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +annotation class AuthenticatorTypes
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
    new file mode 100644
    index 0000000..c63780c
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptData.kt
    
    @@ -0,0 +1,250 @@
    +/*
    + * 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 androidx.credentials.provider
    +
    +import android.hardware.biometrics.BiometricManager
    +import android.hardware.biometrics.BiometricManager.Authenticators
    +import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG
    +import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK
    +import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL
    +import android.hardware.biometrics.BiometricPrompt
    +import android.hardware.biometrics.BiometricPrompt.CryptoObject
    +import android.os.Bundle
    +import android.util.Log
    +import androidx.annotation.RestrictTo
    +
    +/**
    + * Biometric prompt data that can be optionally used by you to provide information needed for the
    + * system to show a biometric prompt directly embedded into the Credential Manager selector.
    + *
    + * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]
    + * will be shown along with a biometric / device credential capture mechanism, on a single dialog,
    + * hence avoiding navigation through multiple screens. When user confirmation is retrieved
    + * through the aforementioned biometric / device capture mechanism, the [android.app.PendingIntent]
    + * associated with the entry is invoked, and the flow continues as explained in [CreateEntry] or
    + * [CredentialEntry].
    + *
    + * Note that the value of [allowedAuthenticators] together with the features of a given device,
    + * determines whether a biometric auth or a device credential mechanism will / can be shown. It is
    + * recommended you use [Authenticators] to select these values, though you can find equivalent
    + * behaviour from usage of [BiometricManager.Authenticators]. This documentation will refer to
    + * [Authenticators] constants, which easily map to their [BiometricManager.Authenticators]
    + * counterparts, and in some cases, provide a more useful abstraction.
    + *
    + * @property allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
    + * the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if
    + * not set
    + * @property cryptoObject a crypto object to be unlocked after successful authentication; When set,
    + * the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else
    + * an [IllegalArgumentException] is thrown
    + *
    + * @throws IllegalArgumentException if [cryptoObject] is not null, and the [allowedAuthenticators]
    + * is not set to [BIOMETRIC_STRONG]
    + *
    + * @see Authenticators
    + * @see BiometricManager.Authenticators
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +class BiometricPromptData internal constructor(
    +    val cryptoObject: BiometricPrompt.CryptoObject? = null,
    +    val allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK,
    +    private var isCreatedFromBundle: Boolean = false,
    +) {
    +
    +    /**
    +     * Biometric prompt data that can be optionally used by you to provide information needed for the
    +     * system to show a biometric prompt directly embedded into the Credential Manager selector.
    +     *
    +     * If you opt to use this, the meta-data provided through the [CreateEntry] or [CredentialEntry]
    +     * will be shown along with a biometric / device credential capture mechanism, on a single dialog,
    +     * hence avoiding navigation through multiple screens. When user confirmation is retrieved
    +     * through the aforementioned biometric / device capture mechanism, the [android.app.PendingIntent]
    +     * associated with the entry is invoked, and the flow continues as explained in [CreateEntry] or
    +     * [CredentialEntry].
    +     *
    +     * Note that the value of [allowedAuthenticators] together with the features of a given device,
    +     * determines whether a biometric auth or a device credential mechanism will / can be shown. It is
    +     * recommended you use [Authenticators] to select these values, though you can find equivalent
    +     * behaviour from usage of [BiometricManager.Authenticators]. This documentation will refer to
    +     * [Authenticators] constants, which easily map to their [BiometricManager.Authenticators]
    +     * counterparts, and in some cases, provide a more useful abstraction.
    +     *
    +     * If you opt to use this constructor, you are confirming you are not building from a slice.
    +     *
    +     * @param allowedAuthenticators specifies the type(s) of authenticators that may be invoked by
    +     * the [BiometricPrompt] to authenticate the user, defaults to [BIOMETRIC_WEAK] if
    +     * not set
    +     * @param cryptoObject a crypto object to be unlocked after successful authentication; When set,
    +     * the value of [allowedAuthenticators] must be [BIOMETRIC_STRONG] or else
    +     * an [IllegalArgumentException] is thrown
    +     *
    +     * @throws IllegalArgumentException If [cryptoObject] is not null, and the [allowedAuthenticators]
    +     * is not set to [BIOMETRIC_STRONG]
    +     *
    +     * @see Authenticators
    +     * @see BiometricManager.Authenticators
    +     */
    +    constructor(
    +        cryptoObject: BiometricPrompt.CryptoObject? = null,
    +        allowedAuthenticators: @AuthenticatorTypes Int = BIOMETRIC_WEAK
    +    ) : this(cryptoObject, allowedAuthenticators, isCreatedFromBundle = false)
    +
    +    init {
    +        if (!isCreatedFromBundle) {
    +            // This is not expected to throw for certain eligible callers who utilize the
    +            // isCreatedFromBundle hidden property.
    +            require(ALLOWED_AUTHENTICATOR_VALUES.contains(allowedAuthenticators)) {
    +                "The allowed authenticator must be specified according to the BiometricPrompt spec."
    +            }
    +        }
    +        if (cryptoObject != null) {
    +            require(isStrongAuthenticationType(allowedAuthenticators)) {
    +                "If the cryptoObject is non-null, the allowedAuthenticator value must be " +
    +                    "Authenticators.BIOMETRIC_STRONG."
    +            }
    +        }
    +    }
    +
    +    internal companion object {
    +
    +        private const val TAG = "BiometricPromptData"
    +
    +        internal const val BUNDLE_HINT_ALLOWED_AUTHENTICATORS =
    +            "androidx.credentials.provider.credentialEntry.BUNDLE_HINT_ALLOWED_AUTHENTICATORS"
    +
    +        internal const val BUNDLE_HINT_CRYPTO_OP_ID =
    +            "androidx.credentials.provider.credentialEntry.BUNDLE_HINT_CRYPTO_OP_ID"
    +
    +        /**
    +         * Returns an instance of [BiometricPromptData] derived from a [Bundle] object.
    +         *
    +         * @param bundle the [Bundle] object constructed through [toBundle] method, often
    +         */
    +        // TODO(b/333444288) : Once available from BiometricPrompt, structure CryptoObject / opId
    +        @JvmStatic
    +        @RestrictTo(RestrictTo.Scope.LIBRARY)
    +        fun fromBundle(bundle: Bundle): BiometricPromptData? {
    +            return try {
    +                if (!bundle.containsKey(BUNDLE_HINT_ALLOWED_AUTHENTICATORS)) {
    +                    throw IllegalArgumentException("Bundle lacks allowed authenticator key.")
    +                }
    +                BiometricPromptData(allowedAuthenticators = bundle.getInt(
    +                    BUNDLE_HINT_ALLOWED_AUTHENTICATORS), isCreatedFromBundle = true)
    +            } catch (e: Exception) {
    +                Log.i(TAG, "fromSlice failed with: " + e.message)
    +                null
    +            }
    +        }
    +
    +        /**
    +         * Returns a [Bundle] that contains the [BiometricPromptData] representation.
    +         */
    +        @JvmStatic
    +        @RestrictTo(RestrictTo.Scope.LIBRARY)
    +        fun toBundle(biometricPromptData: BiometricPromptData): Bundle? {
    +            val bundle = Bundle()
    +            val biometricPromptMap: MutableMap = mutableMapOf(
    +                BUNDLE_HINT_ALLOWED_AUTHENTICATORS to biometricPromptData.allowedAuthenticators,
    +                // TODO(b/325469910) : Use the proper opId method when available
    +                BUNDLE_HINT_CRYPTO_OP_ID to Integer.MIN_VALUE
    +            )
    +            biometricPromptMap.forEach {
    +                if (it.value != null) {
    +                    bundle.putInt(it.key, it.value!!)
    +                }
    +            }
    +            return bundle
    +        }
    +
    +        private fun isStrongAuthenticationType(authenticationTypes: Int?): Boolean {
    +            if (authenticationTypes == null) {
    +                return false
    +            }
    +            val biometricStrength: Int = authenticationTypes and BIOMETRIC_WEAK
    +            if (biometricStrength and BiometricManager.Authenticators.BIOMETRIC_STRONG.inv() != 0) {
    +                return false
    +            }
    +            return true
    +        }
    +
    +        private val ALLOWED_AUTHENTICATOR_VALUES = setOf(
    +            BIOMETRIC_STRONG,
    +            BIOMETRIC_WEAK,
    +            DEVICE_CREDENTIAL,
    +            BIOMETRIC_STRONG or DEVICE_CREDENTIAL,
    +            BIOMETRIC_WEAK or DEVICE_CREDENTIAL
    +        )
    +    }
    +
    +    /** Builder for constructing an instance of [BiometricPromptData] */
    +    class Builder {
    +        private var cryptoObject: CryptoObject? = null
    +        private var allowedAuthenticators: Int? = null
    +
    +        /**
    +         * Sets whether this [BiometricPromptData] should have a crypto object associated with this
    +         * authentication. If opting to pass in a value for cryptoObject, it must not be null.
    +         *
    +         * @param cryptoObject the [CryptoObject] to be associated with this biometric
    +         * authentication flow
    +         */
    +        fun setCryptoObject(cryptoObject: CryptoObject?): Builder {
    +            this.cryptoObject = cryptoObject
    +            return this
    +        }
    +
    +        /**
    +         * Specifies the type(s) of authenticators that may be invoked to authenticate the user.
    +         * Available authenticator types are
    +         * defined in [Authenticators] and can be combined via bitwise OR. Defaults to
    +         * [BIOMETRIC_WEAK].
    +         *
    +         * If this method is used and no authenticator of any of the specified types is
    +         * available at the time an error code will be supplied as part of
    +         * [android.content.Intent] that will be launched by the
    +         * containing [CredentialEntry] or [CreateEntry]'s corresponding
    +         * [android.app.PendingIntent].
    +         *
    +         * @param allowedAuthenticators A bit field representing all valid authenticator types
    +         *                              that may be invoked by the Credential Manager selector.
    +         */
    +        fun setAllowedAuthenticators(allowedAuthenticators: @AuthenticatorTypes Int): Builder {
    +            this.allowedAuthenticators = allowedAuthenticators
    +            return this
    +        }
    +
    +        /**
    +         * Builds the [BiometricPromptData] instance.
    +         *
    +         * @throws IllegalArgumentException If [cryptoObject] is not null, and the
    +         * [allowedAuthenticators] is not set to [BIOMETRIC_STRONG]
    +         */
    +        fun build(): BiometricPromptData {
    +            if (cryptoObject != null && allowedAuthenticators != null) {
    +                require(isStrongAuthenticationType(this.allowedAuthenticators)) {
    +                    "If the cryptoObject is non-null, the allowedAuthenticator value must be " +
    +                        "Authenticators.BIOMETRIC_STRONG"
    +                }
    +            }
    +            val allowedAuthenticators = this.allowedAuthenticators ?: BIOMETRIC_WEAK
    +            return BiometricPromptData(
    +                cryptoObject = cryptoObject,
    +                allowedAuthenticators = allowedAuthenticators,
    +            )
    +        }
    +    }
    +}
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt
    new file mode 100644
    index 0000000..e85404e
    --- /dev/null
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BiometricPromptResult.kt
    
    @@ -0,0 +1,86 @@
    +/*
    + * 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 androidx.credentials.provider
    +
    +import androidx.annotation.RestrictTo
    +import java.util.Objects
    +
    +/**
    + * The result of a Biometric Prompt authentication flow, that is propagated
    + * to the provider if the provider requested for
    + * [androidx.credentials.CredentialManager] to handle the authentication
    + * flow.
    + *
    + * An instance of this class will always be part of the final provider
    + * request, either the [ProviderGetCredentialRequest] or
    + * the [ProviderCreateCredentialRequest] that the provider receives
    + * after the user selects a [CredentialEntry] or a [CreateEntry]
    + * respectively.
    + *
    + * @property isSuccessful whether the result is a success result, in which
    + * case [authenticationResult] should be non-null
    + * @property authenticationResult the result of the authentication flow,
    + * non-null if the authentication flow was successful
    + * @property authenticationError error information, non-null if the
    + * authentication flow has failured, meaning that [isSuccessful] will be false
    + * in this case
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +class BiometricPromptResult internal constructor(
    +    val authenticationResult: AuthenticationResult? = null,
    +    val authenticationError: AuthenticationError? = null
    +) {
    +    val isSuccessful: Boolean = authenticationResult != null
    +
    +    /**
    +     * An unsuccessful biometric prompt result, denoting that
    +     * authentication has failed.
    +     *
    +     * @param authenticationError the error that caused the biometric
    +     * prompt authentication flow to fail
    +     */
    +    constructor(authenticationError: AuthenticationError) : this(
    +        authenticationResult = null, authenticationError = authenticationError
    +    )
    +
    +    /**
    +     * A successful biometric prompt result, denoting that
    +     * authentication has succeeded.
    +     *
    +     * @param authenticationResult the result after a successful biometric
    +     * prompt authentication operation
    +     */
    +    constructor(authenticationResult: AuthenticationResult?) : this(
    +        authenticationResult = authenticationResult, authenticationError = null
    +    )
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) {
    +            return true
    +        }
    +        if (other is BiometricPromptResult) {
    +            return this.isSuccessful == other.isSuccessful &&
    +                this.authenticationResult == other.authenticationResult &&
    +                this.authenticationError == other.authenticationError
    +        }
    +        return false
    +    }
    +
    +    override fun hashCode(): Int {
    +        return Objects.hash(this.isSuccessful, this.authenticationResult, this.authenticationError)
    +    }
    +}
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
    index da5431d..1b07e2e 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
    
    @@ -13,6 +13,8 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
    +
     package androidx.credentials.provider
     
     import android.annotation.SuppressLint
    @@ -29,6 +31,7 @@
     import androidx.credentials.CredentialManager
     import androidx.credentials.PasswordCredential
     import androidx.credentials.PublicKeyCredential
    +import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
     import java.time.Instant
     import java.util.Collections
     
    @@ -40,9 +43,26 @@
      * registered. When user selects this entry, the corresponding [PendingIntent] is fired, and the
      * credential creation can be completed.
      *
    + * @property accountName the name of the account where the credential will be saved
    + * @property pendingIntent the [PendingIntent] that will get invoked when the user selects this
    + * entry, must be created with a unique request code per entry,
    + * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the
    + * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple
    + * times
    + * @property description the localized description shown on UI about where the credential is stored
    + * @property icon the icon to be displayed with this entry on the UI, must be created using
    + * [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as
    + * the latter consumes more memory and may cause undefined behavior due to memory implications
    + * on internal transactions
    + * @property lastUsedTime the last time the account underlying this entry was used by the user,
    + * distinguishable up to the milli second mark only such that if two entries have the same
    + * millisecond precision, they will be considered to have been used at the same time
    + * @property isAutoSelectAllowed whether this entry should be auto selected if it is the only
    + * entry on the selector
    + *
      * @throws IllegalArgumentException If [accountName] is empty
      */
    -@RequiresApi(26)
    +@RequiresApi(23)
     class CreateEntry internal constructor(
         val accountName: CharSequence,
         val pendingIntent: PendingIntent,
    @@ -50,7 +70,9 @@
         val description: CharSequence?,
         val lastUsedTime: Instant?,
         private val credentialCountInformationMap: MutableMap,
    -    val isAutoSelectAllowed: Boolean
    +    val isAutoSelectAllowed: Boolean,
    +    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
    +    val biometricPromptData: BiometricPromptData? = null,
     ) {
     
         /**
    @@ -83,6 +105,65 @@
          * this limit)
          * @throws NullPointerException If [accountName] or [pendingIntent] is null
          */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
    +        accountName: CharSequence,
    +        pendingIntent: PendingIntent,
    +        description: CharSequence? = null,
    +        lastUsedTime: Instant? = null,
    +        icon: Icon? = null,
    +        @Suppress("AutoBoxing")
    +        passwordCredentialCount: Int? = null,
    +        @Suppress("AutoBoxing")
    +        publicKeyCredentialCount: Int? = null,
    +        @Suppress("AutoBoxing")
    +        totalCredentialCount: Int? = null,
    +        isAutoSelectAllowed: Boolean = false,
    +        biometricPromptData: BiometricPromptData? = null,
    +    ) : this(
    +        accountName,
    +        pendingIntent,
    +        icon,
    +        description,
    +        lastUsedTime,
    +        mutableMapOf(
    +            PasswordCredential.TYPE_PASSWORD_CREDENTIAL to passwordCredentialCount,
    +            PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL to publicKeyCredentialCount,
    +            TYPE_TOTAL_CREDENTIAL to totalCredentialCount
    +        ),
    +        isAutoSelectAllowed,
    +        biometricPromptData = biometricPromptData,
    +    )
    +
    +    /**
    +     * Creates an entry to be displayed on the selector during create flows.
    +     *
    +     * @constructor constructs an instance of [CreateEntry]
    +     *
    +     * @param accountName the name of the account where the credential will be saved
    +     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
    +     * entry, must be created with a unique request code per entry,
    +     * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the
    +     * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple
    +     * times
    +     * @param description the localized description shown on UI about where the credential is stored
    +     * @param icon the icon to be displayed with this entry on the UI, must be created using
    +     * [Icon.createWithResource] when possible, and especially not with [Icon.createWithBitmap] as
    +     * the latter consumes more memory and may cause undefined behavior due to memory implications
    +     * on internal transactions
    +     * @param lastUsedTime the last time the account underlying this entry was used by the user,
    +     * distinguishable up to the milli second mark only such that if two entries have the same
    +     * millisecond precision, they will be considered to have been used at the same time
    +     * @param passwordCredentialCount the no. of password credentials contained by the provider
    +     * @param publicKeyCredentialCount the no. of public key credentials contained by the provider
    +     * @param totalCredentialCount the total no. of credentials contained by the provider
    +     * @param isAutoSelectAllowed whether this entry should be auto selected if it is the only
    +     * entry on the selector
    +     *
    +     * @throws IllegalArgumentException If [accountName] is empty, or if [description] is longer
    +     * than 300 characters (important: make sure your descriptions across all locales are within
    +     * this limit)
    +     * @throws NullPointerException If [accountName] or [pendingIntent] is null
    +     */
         constructor(
             accountName: CharSequence,
             pendingIntent: PendingIntent,
    @@ -95,7 +176,7 @@
             publicKeyCredentialCount: Int? = null,
             @Suppress("AutoBoxing")
             totalCredentialCount: Int? = null,
    -        isAutoSelectAllowed: Boolean = false
    +        isAutoSelectAllowed: Boolean = false,
         ) : this(
             accountName,
             pendingIntent,
    @@ -168,6 +249,7 @@
             private var publicKeyCredentialCount: Int? = null
             private var totalCredentialCount: Int? = null
             private var autoSelectAllowed: Boolean = false
    +        private var biometricPromptData: BiometricPromptData? = null
     
             /**
              * Sets whether the entry should be auto-selected.
    @@ -250,14 +332,31 @@
             }
     
             /**
    +         * Sets the biometric prompt data to optionally utilize a credential
    +         * manager flow that directly handles the biometric verification for you and gives you the
    +         * response; set to null by default.
    +         */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY)
    +        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
    +            this.biometricPromptData = biometricPromptData
    +            return this
    +        }
    +
    +        /**
              * Builds an instance of [CreateEntry]
              *
              * @throws IllegalArgumentException If [accountName] is empty
              */
             fun build(): CreateEntry {
                 return CreateEntry(
    -                accountName, pendingIntent, icon, description, lastUsedTime,
    -                credentialCountInformationMap, autoSelectAllowed
    +                accountName = accountName,
    +                pendingIntent = pendingIntent,
    +                icon = icon,
    +                description = description,
    +                lastUsedTime = lastUsedTime,
    +                credentialCountInformationMap = credentialCountInformationMap,
    +                isAutoSelectAllowed = autoSelectAllowed,
    +                biometricPromptData = biometricPromptData
                 )
             }
         }
    @@ -287,6 +386,7 @@
                 val pendingIntent = createEntry.pendingIntent
                 val sliceBuilder = Slice.Builder(Uri.EMPTY,
                     SliceSpec(SLICE_SPEC_TYPE, REVISION_ID))
    +            val biometricPromptData = createEntry.biometricPromptData
     
                 val autoSelectAllowed = if (createEntry.isAutoSelectAllowed) {
                     AUTO_SELECT_TRUE_STRING
    @@ -294,6 +394,9 @@
                     AUTO_SELECT_FALSE_STRING
                 }
     
    +            val cryptoObject = biometricPromptData?.cryptoObject
    +            val allowedAuthenticators = biometricPromptData?.allowedAuthenticators
    +
                 sliceBuilder.addText(
                     accountName, /*subType=*/null,
                     listOf(SLICE_HINT_ACCOUNT_NAME)
    @@ -339,6 +442,25 @@
                     autoSelectAllowed, /*subType=*/null,
                     listOf(SLICE_HINT_AUTO_SELECT_ALLOWED)
                 )
    +
    +            if (biometricPromptData != null) {
    +                // TODO(b/326243730) : Await biometric team dependency for opId, then add
    +                val cryptoObjectOpId = cryptoObject?.hashCode()
    +
    +                if (allowedAuthenticators != null) {
    +                    sliceBuilder.addInt(
    +                        allowedAuthenticators, /*subType=*/null,
    +                        listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
    +                    )
    +                }
    +                if (cryptoObjectOpId != null) {
    +                    sliceBuilder.addInt(
    +                        cryptoObjectOpId, /*subType=*/null,
    +                        listOf(SLICE_HINT_CRYPTO_OP_ID)
    +                    )
    +                }
    +            }
    +
                 return sliceBuilder.build()
             }
     
    @@ -353,6 +475,8 @@
                 var description: CharSequence? = null
                 var lastUsedTime: Instant? = null
                 var autoSelectAllowed = false
    +            var allowedAuth: Int? = null
    +
                 slice.items.forEach {
                     if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {
                         accountName = it.text
    @@ -374,12 +498,30 @@
                         if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
                             autoSelectAllowed = true
                         }
    +                } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {
    +                    allowedAuth = it.int
                     }
                 }
    +
    +            // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide
    +            // if we want toBundle to be passed into the framework.
    +            var biometricPromptDataBundle: Bundle? = null
    +            if (allowedAuth != null) {
    +                biometricPromptDataBundle = Bundle()
    +                biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)
    +            }
    +
                 return try {
                     CreateEntry(
    -                    accountName!!, pendingIntent!!, icon, description,
    -                    lastUsedTime, credentialCountInfo, autoSelectAllowed
    +                    accountName = accountName!!,
    +                    pendingIntent = pendingIntent!!,
    +                    icon = icon,
    +                    description = description,
    +                    lastUsedTime = lastUsedTime,
    +                    credentialCountInformationMap = credentialCountInfo,
    +                    isAutoSelectAllowed = autoSelectAllowed,
    +                    biometricPromptData = if (biometricPromptDataBundle != null)
    +                        BiometricPromptData.fromBundle(biometricPromptDataBundle) else null
                     )
                 } catch (e: Exception) {
                     Log.i(TAG, "fromSlice failed with: " + e.message)
    @@ -450,6 +592,12 @@
             private const val SLICE_HINT_AUTO_SELECT_ALLOWED =
                 "androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_SELECT_ALLOWED"
     
    +        private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
    +            "androidx.credentials.provider.createEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
    +
    +        private const val SLICE_HINT_CRYPTO_OP_ID =
    +            "androidx.credentials.provider.createEntry.SLICE_HINT_CRYPTO_OP_ID"
    +
             private const val AUTO_SELECT_TRUE_STRING = "true"
     
             private const val AUTO_SELECT_FALSE_STRING = "false"
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
    index eb93143..831d6a3 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
    
    @@ -13,6 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
     
     package androidx.credentials.provider
     
    @@ -58,6 +59,8 @@
         val entryGroupId: CharSequence,
         val isDefaultIconPreferredAsSingleProvider: Boolean,
         val affiliatedDomain: CharSequence? = null,
    +    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
    +    val biometricPromptData: BiometricPromptData? = null,
     ) {
     
         @RequiresApi(34)
    @@ -69,6 +72,7 @@
                 return fromSlice(slice)
             }
         }
    +
         companion object {
     
             /**
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
    index d9f1227..3737d57 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CustomCredentialEntry.kt
    
    @@ -13,8 +13,9 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    -package androidx.credentials.provider
    +@file:Suppress("deprecation") // For usage of Slice
     
    +package androidx.credentials.provider
     import android.annotation.SuppressLint
     import android.app.PendingIntent
     import android.app.slice.Slice
    @@ -29,6 +30,7 @@
     import androidx.annotation.RestrictTo
     import androidx.credentials.CredentialOption
     import androidx.credentials.R
    +import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
     import java.time.Instant
     import java.util.Collections
     
    @@ -74,7 +76,7 @@
      *
      * @see CredentialEntry
      */
    -@RequiresApi(26)
    +@RequiresApi(23)
     class CustomCredentialEntry internal constructor(
         override val type: String,
         val title: CharSequence,
    @@ -89,16 +91,18 @@
         isDefaultIconPreferredAsSingleProvider: Boolean,
         entryGroupId: CharSequence? = title,
         affiliatedDomain: CharSequence? = null,
    +    biometricPromptData: BiometricPromptData? = null,
         autoSelectAllowedFromOption: Boolean = CredentialOption.extractAutoSelectValue(
             beginGetCredentialOption.candidateQueryData),
         private var isCreatedFromSlice: Boolean = false,
         private var isDefaultIconFromSlice: Boolean = false,
     ) : CredentialEntry(
    -    type,
    -    beginGetCredentialOption,
    -    entryGroupId ?: title,
    +    type = type,
    +    beginGetCredentialOption = beginGetCredentialOption,
    +    entryGroupId = entryGroupId ?: title,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         affiliatedDomain = affiliatedDomain,
    +    biometricPromptData = biometricPromptData
     ) {
         val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
     
    @@ -159,15 +163,15 @@
             @Suppress("AutoBoxing")
             isAutoSelectAllowed: Boolean = false,
         ) : this(
    -        beginGetCredentialOption.type,
    -        title,
    -        pendingIntent,
    -        isAutoSelectAllowed,
    -        subtitle,
    -        typeDisplayName,
    -        icon,
    -        lastUsedTime,
    -        beginGetCredentialOption,
    +        type = beginGetCredentialOption.type,
    +        title = title,
    +        pendingIntent = pendingIntent,
    +        isAutoSelectAllowed = isAutoSelectAllowed,
    +        subtitle = subtitle,
    +        typeDisplayName = typeDisplayName,
    +        icon = icon,
    +        lastUsedTime = lastUsedTime,
    +        beginGetCredentialOption = beginGetCredentialOption,
             isDefaultIconPreferredAsSingleProvider = false
         )
     
    @@ -213,7 +217,65 @@
             @Suppress("AutoBoxing")
             isAutoSelectAllowed: Boolean = false,
             entryGroupId: CharSequence = title,
    -        isDefaultIconPreferredAsSingleProvider: Boolean = false
    +        isDefaultIconPreferredAsSingleProvider: Boolean = false,
    +    ) : this(
    +        type = beginGetCredentialOption.type,
    +        title = title,
    +        pendingIntent = pendingIntent,
    +        isAutoSelectAllowed = isAutoSelectAllowed,
    +        subtitle = subtitle,
    +        typeDisplayName = typeDisplayName,
    +        icon = icon,
    +        lastUsedTime = lastUsedTime,
    +        beginGetCredentialOption = beginGetCredentialOption,
    +        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
    +        entryGroupId = entryGroupId.ifEmpty { title },
    +    )
    +
    +    /**
    +     * @constructor constructs an instance of [CustomCredentialEntry]
    +     *
    +     * @param context the context of the calling app, required to retrieve fallback resources
    +     * @param title the title shown with this entry on the selector UI
    +     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
    +     * entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android
    +     * system to attach the final request
    +     * @param beginGetCredentialOption the option from the original [BeginGetCredentialRequest],
    +     * for which this credential entry is being added
    +     * @param subtitle the subTitle shown with this entry on the selector UI
    +     * @param lastUsedTime the last used time the credential underlying this entry was
    +     * used by the user, distinguishable up to the milli second mark only such that if two
    +     * entries have the same millisecond precision, they will be considered to have been used at
    +     * the same time
    +     * @param typeDisplayName the friendly name to be displayed on the UI for
    +     * the type of the credential
    +     * @param icon the icon to be displayed with this entry on the selector UI, if not set a
    +     * default icon representing a custom credential type is set by the library
    +     * @param isAutoSelectAllowed whether this entry is allowed to be auto
    +     * selected if it is the only one on the UI, only takes effect if the app requesting for
    +     * credentials also opts for auto select
    +     * @param entryGroupId an ID to uniquely mark this entry for deduplication or to group entries
    +     * during display, set to [title] by default
    +     * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
    +     * default credential type icon (see the default value of [icon]) when you are
    +     * the only available provider; false by default
    +     *
    +     * @throws IllegalArgumentException If [type] or [title] are empty
    +     */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
    +        context: Context,
    +        title: CharSequence,
    +        pendingIntent: PendingIntent,
    +        beginGetCredentialOption: BeginGetCredentialOption,
    +        subtitle: CharSequence? = null,
    +        typeDisplayName: CharSequence? = null,
    +        lastUsedTime: Instant? = null,
    +        icon: Icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in),
    +        @Suppress("AutoBoxing")
    +        isAutoSelectAllowed: Boolean = false,
    +        entryGroupId: CharSequence = title,
    +        isDefaultIconPreferredAsSingleProvider: Boolean = false,
    +        biometricPromptData: BiometricPromptData? = null,
         ) : this(
             beginGetCredentialOption.type,
             title,
    @@ -225,7 +287,8 @@
             lastUsedTime,
             beginGetCredentialOption,
             isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
    -        entryGroupId.ifEmpty { title },
    +        entryGroupId = entryGroupId.ifEmpty { title },
    +        biometricPromptData = biometricPromptData,
         )
     
         @RequiresApi(34)
    @@ -267,18 +330,19 @@
                 val affiliatedDomain = entry.affiliatedDomain
                 val isDefaultIconPreferredAsSingleProvider =
                     entry.isDefaultIconPreferredAsSingleProvider
    -
    +            val biometricPromptData = entry.biometricPromptData
                 val autoSelectAllowed = if (isAutoSelectAllowed) {
                     TRUE_STRING
                 } else {
                     FALSE_STRING
                 }
    -
                 val isUsingDefaultIconPreferred = if (isDefaultIconPreferredAsSingleProvider) {
                     TRUE_STRING
                 } else {
                     FALSE_STRING
                 }
    +            val allowedAuthenticators = biometricPromptData?.allowedAuthenticators
    +            val cryptoObject = biometricPromptData?.cryptoObject
                 val sliceBuilder = Slice.Builder(
                     Uri.EMPTY, SliceSpec(
                         type, REVISION_ID
    @@ -321,7 +385,6 @@
                         isUsingDefaultIconPreferred, /*subType=*/null,
                         listOf(SLICE_HINT_IS_DEFAULT_ICON_PREFERRED)
                     )
    -
                 try {
                     if (entry.hasDefaultIcon) {
                         sliceBuilder.addInt(
    @@ -332,7 +395,6 @@
                     }
                 } catch (_: IllegalStateException) {
                 }
    -
                 if (entry.isAutoSelectAllowedFromOption) {
                     sliceBuilder.addInt(
                         /*true=*/1,
    @@ -354,6 +416,24 @@
                         .build(),
                     /*subType=*/null
                 )
    +
    +            if (biometricPromptData != null) {
    +                // TODO(b/326243730) : Await biometric team dependency for opId, then add
    +                val cryptoObjectOpId = cryptoObject?.hashCode()
    +
    +                if (allowedAuthenticators != null) {
    +                    sliceBuilder.addInt(
    +                        allowedAuthenticators, /*subType=*/null,
    +                        listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
    +                    )
    +                }
    +                if (cryptoObjectOpId != null) {
    +                    sliceBuilder.addInt(
    +                        cryptoObjectOpId, /*subType=*/null,
    +                        listOf(SLICE_HINT_CRYPTO_OP_ID)
    +                    )
    +                }
    +            }
                 return sliceBuilder.build()
             }
     
    @@ -381,7 +461,7 @@
                 var isDefaultIconPreferredAsSingleProvider = false
                 var isDefaultIcon = false
                 var affiliatedDomain: CharSequence? = null
    -
    +            var allowedAuth: Int? = null
                 slice.items.forEach {
                     if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
                         typeDisplayName = it.text
    @@ -415,9 +495,19 @@
                         isDefaultIcon = true
                     } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
                         affiliatedDomain = it.text
    +                } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {
    +                    allowedAuth = it.int
                     }
                 }
     
    +            // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide
    +            // if we want toBundle to be passed into the framework.
    +            var biometricPromptDataBundle: Bundle? = null
    +            if (allowedAuth != null) {
    +                biometricPromptDataBundle = Bundle()
    +                biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)
    +            }
    +
                 return try {
                     CustomCredentialEntry(
                         type = type,
    @@ -439,6 +529,8 @@
                         autoSelectAllowedFromOption = autoSelectAllowedFromOption,
                         isCreatedFromSlice = true,
                         isDefaultIconFromSlice = isDefaultIcon,
    +                    biometricPromptData = if (biometricPromptDataBundle != null)
    +                        BiometricPromptData.fromBundle(biometricPromptDataBundle) else null
                     )
                 } catch (e: Exception) {
                     Log.i(TAG, "fromSlice failed with: " + e.message)
    @@ -489,6 +581,12 @@
             private const val SLICE_HINT_DEFAULT_ICON_RES_ID =
                 "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEFAULT_ICON_RES_ID"
     
    +        private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
    +            "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
    +
    +        private const val SLICE_HINT_CRYPTO_OP_ID =
    +            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"
    +
             private const val TRUE_STRING = "true"
     
             private const val FALSE_STRING = "false"
    @@ -512,7 +610,6 @@
                 }
                 return null
             }
    -
             /**
              * Returns an instance of [CustomCredentialEntry] derived from a [Slice] object.
              *
    @@ -528,7 +625,6 @@
                 }
                 return null
             }
    -
             /**
              * Converts a framework [android.service.credentials.CredentialEntry] class to a Jetpack
              * [CustomCredentialEntry] class
    @@ -544,7 +640,6 @@
                 return null
             }
         }
    -
         /**
          * Builder for [CustomCredentialEntry]
          *
    @@ -576,6 +671,7 @@
             private var autoSelectAllowed = false
             private var entryGroupId: CharSequence = title
             private var isDefaultIconPreferredAsSingleProvider = false
    +        private var biometricPromptData: BiometricPromptData? = null
     
             /** Sets a displayName to be shown on the UI with this entry. */
             fun setSubtitle(subtitle: CharSequence?): Builder {
    @@ -599,6 +695,17 @@
             }
     
             /**
    +         * Sets the biometric prompt data to optionally utilize a credential
    +         * manager flow that directly handles the biometric verification for you and gives you the
    +         * response; set to null by default.
    +         */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY)
    +        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
    +            this.biometricPromptData = biometricPromptData
    +            return this
    +        }
    +
    +        /**
              * Sets whether the entry should be auto-selected.
              * The value is false by default.
              */
    @@ -646,17 +753,18 @@
                     icon = Icon.createWithResource(context, R.drawable.ic_other_sign_in)
                 }
                 return CustomCredentialEntry(
    -                type,
    -                title,
    -                pendingIntent,
    -                autoSelectAllowed,
    -                subtitle,
    -                typeDisplayName,
    -                icon!!,
    -                lastUsedTime,
    -                beginGetCredentialOption,
    +                type = type,
    +                title = title,
    +                pendingIntent = pendingIntent,
    +                isAutoSelectAllowed = autoSelectAllowed,
    +                subtitle = subtitle,
    +                typeDisplayName = typeDisplayName,
    +                icon = icon!!,
    +                lastUsedTime = lastUsedTime,
    +                beginGetCredentialOption = beginGetCredentialOption,
                     isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
                     entryGroupId = entryGroupId,
    +                biometricPromptData = biometricPromptData,
                 )
             }
         }
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
    index b78ae4e..bc2754b 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
    
    @@ -13,6 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
     
     package androidx.credentials.provider
     
    @@ -31,6 +32,7 @@
     import androidx.credentials.CredentialOption
     import androidx.credentials.PasswordCredential
     import androidx.credentials.R
    +import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
     import androidx.credentials.provider.PasswordCredentialEntry.Companion.toSlice
     import java.time.Instant
     import java.util.Collections
    @@ -71,7 +73,7 @@
      * @see CustomCredentialEntry
      * @see CredentialEntry
      */
    -@RequiresApi(26)
    +@RequiresApi(23)
     class PasswordCredentialEntry internal constructor(
         val username: CharSequence,
         val displayName: CharSequence?,
    @@ -84,16 +86,18 @@
         isDefaultIconPreferredAsSingleProvider: Boolean,
         entryGroupId: CharSequence? = username,
         affiliatedDomain: CharSequence? = null,
    +    biometricPromptData: BiometricPromptData? = null,
         autoSelectAllowedFromOption: Boolean = CredentialOption.extractAutoSelectValue(
             beginGetPasswordOption.candidateQueryData),
         private var isCreatedFromSlice: Boolean = false,
         private var isDefaultIconFromSlice: Boolean = false
     ) : CredentialEntry(
    -    PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
    -    beginGetPasswordOption,
    -    entryGroupId ?: username,
    +    type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
    +    beginGetCredentialOption = beginGetPasswordOption,
    +    entryGroupId = entryGroupId ?: username,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         affiliatedDomain = affiliatedDomain,
    +    biometricPromptData = biometricPromptData,
     ) {
     
         val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
    @@ -147,6 +151,70 @@
          * @throws NullPointerException If [context], [username], [pendingIntent], or
          * [beginGetPasswordOption] is null
          */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
    +        context: Context,
    +        username: CharSequence,
    +        pendingIntent: PendingIntent,
    +        beginGetPasswordOption: BeginGetPasswordOption,
    +        displayName: CharSequence? = null,
    +        lastUsedTime: Instant? = null,
    +        icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),
    +        isAutoSelectAllowed: Boolean = false,
    +        affiliatedDomain: CharSequence? = null,
    +        isDefaultIconPreferredAsSingleProvider: Boolean = false,
    +        biometricPromptData: BiometricPromptData? = null
    +    ) : this(
    +        username,
    +        displayName,
    +        typeDisplayName = context.getString(
    +            R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
    +        ),
    +        pendingIntent,
    +        lastUsedTime,
    +        icon,
    +        isAutoSelectAllowed,
    +        beginGetPasswordOption,
    +        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
    +        affiliatedDomain = affiliatedDomain,
    +        biometricPromptData = biometricPromptData,
    +    )
    +
    +    /**
    +     * @constructor constructs an instance of [PasswordCredentialEntry]
    +     *
    +     * The [affiliatedDomain] parameter is filled if you provide a credential
    +     * that is not directly associated with the requesting entity, but rather originates from an
    +     * entity that is determined as being associated with the requesting entity through mechanisms
    +     * such as digital asset links.
    +     *
    +     * @param context the context of the calling app, required to retrieve fallback resources
    +     * @param username the username of the account holding the password credential
    +     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
    +     * entry, must be created with flag [PendingIntent.FLAG_MUTABLE] to allow the Android
    +     * system to attach the final request
    +     * @param beginGetPasswordOption the option from the original [BeginGetCredentialRequest],
    +     * for which this credential entry is being added
    +     * @param displayName the displayName of the account holding the password credential
    +     * @param lastUsedTime the last used time the credential underlying this entry was
    +     * used by the user, distinguishable up to the milli second mark only such that if two
    +     * entries have the same millisecond precision, they will be considered to have been used at
    +     * the same time
    +     * @param icon the icon to be displayed with this entry on the selector, if not set, a
    +     * default icon representing a password credential type is set by the library
    +     * @param isAutoSelectAllowed whether this entry is allowed to be auto
    +     * selected if it is the only one on the UI, only takes effect if the app requesting for
    +     * credentials also opts for auto select
    +     * @param affiliatedDomain the user visible affiliated domain, a CharSequence
    +     * representation of a web domain or an app package name that the given credential in this
    +     * entry is associated with when it is different from the requesting entity, default null
    +     * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
    +     * default credential type icon (see the default value of [icon]) when you are the
    +     * only available provider; false by default
    +     *
    +     * @throws IllegalArgumentException If [username] is empty
    +     * @throws NullPointerException If [context], [username], [pendingIntent], or
    +     * [beginGetPasswordOption] is null
    +     */
         constructor(
             context: Context,
             username: CharSequence,
    @@ -159,16 +227,16 @@
             affiliatedDomain: CharSequence? = null,
             isDefaultIconPreferredAsSingleProvider: Boolean = false,
         ) : this(
    -        username,
    -        displayName,
    +        username = username,
    +        displayName = displayName,
             typeDisplayName = context.getString(
                 R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
             ),
    -        pendingIntent,
    -        lastUsedTime,
    -        icon,
    -        isAutoSelectAllowed,
    -        beginGetPasswordOption,
    +        pendingIntent = pendingIntent,
    +        lastUsedTime = lastUsedTime,
    +        icon = icon,
    +        isAutoSelectAllowed = isAutoSelectAllowed,
    +        beginGetPasswordOption = beginGetPasswordOption,
             isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
             affiliatedDomain = affiliatedDomain,
         )
    @@ -217,16 +285,16 @@
             icon: Icon = Icon.createWithResource(context, R.drawable.ic_password),
             isAutoSelectAllowed: Boolean = false
         ) : this(
    -        username,
    -        displayName,
    +        username = username,
    +        displayName = displayName,
             typeDisplayName = context.getString(
                 R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
             ),
    -        pendingIntent,
    -        lastUsedTime,
    -        icon,
    -        isAutoSelectAllowed,
    -        beginGetPasswordOption,
    +        pendingIntent = pendingIntent,
    +        lastUsedTime = lastUsedTime,
    +        icon = icon,
    +        isAutoSelectAllowed = isAutoSelectAllowed,
    +        beginGetPasswordOption = beginGetPasswordOption,
             isDefaultIconPreferredAsSingleProvider = false
         )
     
    @@ -351,6 +419,23 @@
                         .build(),
                     /*subType=*/null
                 )
    +            val biometricPromptData = entry.biometricPromptData
    +            if (biometricPromptData != null) {
    +                val allowedAuthenticators = biometricPromptData.allowedAuthenticators
    +                // TODO(b/326243730) : Await biometric team dependency for opId, then add
    +                val cryptoObjectOpId = biometricPromptData.cryptoObject?.hashCode()
    +
    +                sliceBuilder.addInt(
    +                    allowedAuthenticators, /*subType=*/null,
    +                    listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
    +                )
    +                if (cryptoObjectOpId != null) {
    +                    sliceBuilder.addInt(
    +                        cryptoObjectOpId, /*subType=*/null,
    +                        listOf(SLICE_HINT_CRYPTO_OP_ID)
    +                    )
    +                }
    +            }
                 return sliceBuilder.build()
             }
     
    @@ -377,6 +462,7 @@
                 var affiliatedDomain: CharSequence? = null
                 var entryGroupId: CharSequence? = null
                 var isDefaultIcon = false
    +            var allowedAuth: Int? = null
     
                 slice.items.forEach {
                     if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
    @@ -411,9 +497,19 @@
                         }
                     } else if (it.hasHint(SLICE_HINT_DEFAULT_ICON_RES_ID)) {
                         isDefaultIcon = true
    +                } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {
    +                    allowedAuth = it.int
                     }
                 }
     
    +            // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide
    +            // if we want toBundle to be passed into the framework.
    +            var biometricPromptDataBundle: Bundle? = null
    +            if (allowedAuth != null) {
    +                biometricPromptDataBundle = Bundle()
    +                biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)
    +            }
    +
                 return try {
                     PasswordCredentialEntry(
                         username = title!!,
    @@ -433,6 +529,8 @@
                         autoSelectAllowedFromOption = autoSelectAllowedFromOption,
                         isCreatedFromSlice = true,
                         isDefaultIconFromSlice = isDefaultIcon,
    +                    biometricPromptData = if (biometricPromptDataBundle != null)
    +                        BiometricPromptData.fromBundle(biometricPromptDataBundle) else null
                     )
                 } catch (e: Exception) {
                     Log.i(TAG, "fromSlice failed with: " + e.message)
    @@ -483,6 +581,12 @@
             private const val SLICE_HINT_AFFILIATED_DOMAIN =
                 "androidx.credentials.provider.credentialEntry.SLICE_HINT_AFFILIATED_DOMAIN"
     
    +        private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
    +            "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
    +
    +        private const val SLICE_HINT_CRYPTO_OP_ID =
    +            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"
    +
             private const val TRUE_STRING = "true"
     
             private const val FALSE_STRING = "false"
    @@ -569,6 +673,7 @@
             private var autoSelectAllowed = false
             private var affiliatedDomain: CharSequence? = null
             private var isDefaultIconPreferredAsSingleProvider: Boolean = false
    +        private var biometricPromptData: BiometricPromptData? = null
     
             /** Sets a displayName to be shown on the UI with this entry. */
             fun setDisplayName(displayName: CharSequence?): Builder {
    @@ -583,6 +688,17 @@
             }
     
             /**
    +         * Sets the biometric prompt data to optionally utilize a credential
    +         * manager flow that directly handles the biometric verification for you and gives you the
    +         * response; set to null by default.
    +         */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY)
    +        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
    +            this.biometricPromptData = biometricPromptData
    +            return this
    +        }
    +
    +        /**
              * Sets whether the entry should be auto-selected.
              * The value is false by default.
              */
    @@ -632,16 +748,17 @@
                     R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL
                 )
                 return PasswordCredentialEntry(
    -                username,
    -                displayName,
    -                typeDisplayName,
    -                pendingIntent,
    -                lastUsedTime,
    -                icon!!,
    -                autoSelectAllowed,
    -                beginGetPasswordOption,
    +                username = username,
    +                displayName = displayName,
    +                typeDisplayName = typeDisplayName,
    +                pendingIntent = pendingIntent,
    +                lastUsedTime = lastUsedTime,
    +                icon = icon!!,
    +                isAutoSelectAllowed = autoSelectAllowed,
    +                beginGetPasswordOption = beginGetPasswordOption,
                     isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
                     affiliatedDomain = affiliatedDomain,
    +                biometricPromptData = biometricPromptData,
                 )
             }
         }
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
    index 0bec31c..9543d76 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
    
    @@ -77,9 +77,10 @@
                     Log.i(TAG, "Request not found in pendingIntent")
                     return frameworkReq
                 }
    +            val biometricPromptResult = retrieveBiometricPromptResult(intent)
                 return try {
                     ProviderCreateCredentialRequest(
    -                    androidx.credentials.CreateCredentialRequest
    +                    callingRequest = androidx.credentials.CreateCredentialRequest
                             .createFrom(
                                 frameworkReq.type,
                                 frameworkReq.data,
    @@ -87,17 +88,43 @@
                                 requireSystemProvider = false,
                                 frameworkReq.callingAppInfo.origin
                             ),
    -                    CallingAppInfo(
    +                    callingAppInfo = CallingAppInfo(
                             frameworkReq.callingAppInfo.packageName,
                             frameworkReq.callingAppInfo.signingInfo,
                             frameworkReq.callingAppInfo.origin
    -                    )
    +                    ),
    +                    biometricPromptResult = biometricPromptResult,
    +                    isInternal = false
                     )
                 } catch (e: IllegalArgumentException) {
                     return null
                 }
             }
     
    +        private fun retrieveBiometricPromptResult(intent: Intent): BiometricPromptResult? {
    +            if (intent.extras == null) {
    +                return null
    +            }
    +            if (intent.extras!!.containsKey(AuthenticationResult
    +                    .EXTRA_BIOMETRIC_AUTH_RESULT_TYPE)) {
    +                val authResultType = intent.extras!!.getInt(AuthenticationResult
    +                    .EXTRA_BIOMETRIC_AUTH_RESULT_TYPE)
    +                return BiometricPromptResult(
    +                    authenticationResult = AuthenticationResult(authResultType)
    +                )
    +            } else if (intent.extras!!.containsKey(AuthenticationError
    +                    .EXTRA_BIOMETRIC_AUTH_ERROR)) {
    +                val authResultError = intent.extras!!.getInt(
    +                    AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR)
    +                return BiometricPromptResult(
    +                    authenticationError = AuthenticationError(
    +                        authResultError, intent.extras?.getCharSequence(
    +                            AuthenticationError.EXTRA_BIOMETRIC_AUTH_ERROR_MESSAGE))
    +                )
    +            }
    +            return null
    +        }
    +
             /**
              * Extracts the [BeginGetCredentialRequest] from the provider's
              * [PendingIntent] invoked by the Android system when the user
    @@ -169,6 +196,7 @@
                     Log.i(TAG, "Get request from framework is null")
                     return null
                 }
    +            val biometricPromptResult = retrieveBiometricPromptResult(intent)
     
                 return ProviderGetCredentialRequest.createFrom(
                     frameworkReq.credentialOptions.stream()
    @@ -186,7 +214,8 @@
                         frameworkReq.callingAppInfo.packageName,
                         frameworkReq.callingAppInfo.signingInfo,
                         frameworkReq.callingAppInfo.origin
    -                )
    +                ),
    +                biometricPromptResult
                 )
             }
     
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
    index 3b39ebb..f7f8915 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderCreateCredentialRequest.kt
    
    @@ -16,6 +16,9 @@
     
     package androidx.credentials.provider
     
    +import android.util.Log
    +import androidx.annotation.RestrictTo
    +import androidx.annotation.VisibleForTesting
     import androidx.credentials.CreateCredentialRequest
     
     /**
    @@ -27,9 +30,9 @@
      *
      * @constructor constructs an instance of [ProviderCreateCredentialRequest]
      *
    - * @param callingRequest the complete [CreateCredentialRequest] coming from
    + * @property callingRequest the complete [CreateCredentialRequest] coming from
      * the calling app that is requesting for credential creation
    - * @param callingAppInfo information pertaining to the calling app making
    + * @property callingAppInfo information pertaining to the calling app making
      * the request
      *
      * @throws NullPointerException If [callingRequest], or [callingAppInfo] is null
    @@ -37,7 +40,40 @@
      * Note : Credential providers are not expected to utilize the constructor in this class for any
      * production flow. This constructor must only be used for testing purposes.
      */
    -class ProviderCreateCredentialRequest constructor(
    +class ProviderCreateCredentialRequest internal constructor(
         val callingRequest: CreateCredentialRequest,
    -    val callingAppInfo: CallingAppInfo
    -)
    +    val callingAppInfo: CallingAppInfo,
    +    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    val biometricPromptResult: BiometricPromptResult? = null,
    +    // TODO: Remove when exposing API and make this the only constructor
    +    isInternal: Boolean = true
    +) {
    +    init {
    +        // TODO: Remove when exposing API
    +        Log.i("ProvCrCredRequest", isInternal.toString())
    +    }
    +    /**
    +     * Constructs an instance of this class
    +     *
    +     * @param callingRequest the complete [CreateCredentialRequest] coming from
    +     * the calling app that is requesting for credential creation
    +     * @param callingAppInfo information pertaining to the calling app making
    +     * the request
    +     */
    +    constructor(
    +        callingRequest: CreateCredentialRequest,
    +        callingAppInfo: CallingAppInfo,
    +    ) : this(
    +        callingRequest = callingRequest,
    +        callingAppInfo = callingAppInfo,
    +        isInternal = false
    +    )
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @VisibleForTesting
    +    constructor(
    +        callingRequest: CreateCredentialRequest,
    +        callingAppInfo: CallingAppInfo,
    +        biometricPromptResult: BiometricPromptResult?
    +    ) : this(callingRequest, callingAppInfo, biometricPromptResult, isInternal = false)
    +}
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
    index ae27fd3..742dbe2 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/ProviderGetCredentialRequest.kt
    
    @@ -16,6 +16,8 @@
     package androidx.credentials.provider
     
     import android.app.PendingIntent
    +import android.util.Log
    +import androidx.annotation.RestrictTo
     import androidx.credentials.CredentialOption
     
     /**
    @@ -30,31 +32,76 @@
      *
      * @constructor constructs an instance of [ProviderGetCredentialRequest]
      *
    - * @param credentialOptions the list of credential retrieval options containing the
    + * @property credentialOptions the list of credential retrieval options containing the
      * required parameters, expected  to contain a single [CredentialOption] when this
      * request is retrieved from the [android.app.Activity] invoked by the [android.app.PendingIntent]
      * set on a [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain
      * multiple [CredentialOption] when this request is retrieved
      * from the [android.app.Activity] invoked by the [android.app.PendingIntent]
      * set on a [RemoteEntry]
    - * @param callingAppInfo information pertaining to the calling application
    + * @property callingAppInfo information pertaining to the calling application
      *
      * Note : Credential providers are not expected to utilize the constructor in this class for any
      * production flow. This constructor must only be used for testing purposes.
      */
    -class ProviderGetCredentialRequest constructor(
    +class ProviderGetCredentialRequest internal constructor(
         val credentialOptions: List,
    -    val callingAppInfo: CallingAppInfo
    +    val callingAppInfo: CallingAppInfo,
    +    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    val biometricPromptResult: BiometricPromptResult? = null,
    +    // TODO: Remove when exposing this API
    +    isInternal: Boolean = false
     ) {
    +    init {
    +        // TODO: Remove when exposing API
    +        Log.i("ProvCrCredRequest", isInternal.toString())
    +    }
    +
    +    /**
    +     * Constructs an instance of this class
    +     *
    +     * @param credentialOptions the list of credential retrieval options containing the
    +     * required parameters, expected  to contain a single [CredentialOption] when this
    +     * request is retrieved from the [android.app.Activity] invoked by the [android.app.PendingIntent]
    +     * set on a [PasswordCredentialEntry] or a [PublicKeyCredentialEntry], or expected to contain
    +     * multiple [CredentialOption] when this request is retrieved
    +     * from the [android.app.Activity] invoked by the [android.app.PendingIntent]
    +     * set on a [RemoteEntry]
    +     * @param callingAppInfo information pertaining to the calling application
    +     */
    +    constructor(
    +        credentialOptions: List,
    +        callingAppInfo: CallingAppInfo,
    +    ) : this(
    +        credentialOptions = credentialOptions,
    +        callingAppInfo = callingAppInfo,
    +        isInternal = false
    +    )
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    constructor(
    +        credentialOptions: List,
    +        callingAppInfo: CallingAppInfo,
    +        biometricPromptResult: BiometricPromptResult?
    +    ) : this(
    +        credentialOptions = credentialOptions,
    +        callingAppInfo = callingAppInfo,
    +        biometricPromptResult = biometricPromptResult,
    +        isInternal = false
    +    )
    +
         internal companion object {
             @JvmStatic
             internal fun createFrom(
                 options: List,
    -            callingAppInfo: CallingAppInfo
    +            callingAppInfo: CallingAppInfo,
    +            biometricPromptResult: BiometricPromptResult? = null
             ): ProviderGetCredentialRequest {
                 return ProviderGetCredentialRequest(
                     options,
    -                callingAppInfo)
    +                callingAppInfo,
    +                biometricPromptResult
    +            )
             }
         }
     }
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
    index b41375c..1f37e87 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
    
    @@ -13,6 +13,7 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
     
     package androidx.credentials.provider
     
    @@ -31,6 +32,7 @@
     import androidx.credentials.CredentialOption
     import androidx.credentials.PublicKeyCredential
     import androidx.credentials.R
    +import androidx.credentials.provider.BiometricPromptData.Companion.BUNDLE_HINT_ALLOWED_AUTHENTICATORS
     import androidx.credentials.provider.PublicKeyCredentialEntry.Companion.toSlice
     import java.time.Instant
     import java.util.Collections
    @@ -73,7 +75,8 @@
      *
      * @see CredentialEntry
      */
    -@RequiresApi(26)
    +@RequiresApi(23)
    +@Suppress("DEPRECATION") // For usage of slice
     class PublicKeyCredentialEntry internal constructor(
         val username: CharSequence,
         val displayName: CharSequence?,
    @@ -86,17 +89,19 @@
         isDefaultIconPreferredAsSingleProvider: Boolean,
         entryGroupId: CharSequence? = username,
         affiliatedDomain: CharSequence? = null,
    +    biometricPromptData: BiometricPromptData? = null,
         autoSelectAllowedFromOption: Boolean = CredentialOption.extractAutoSelectValue(
             beginGetPublicKeyCredentialOption.candidateQueryData
         ),
         private val isCreatedFromSlice: Boolean = false,
         private val isDefaultIconFromSlice: Boolean = false,
     ) : CredentialEntry(
    -    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
    -    beginGetPublicKeyCredentialOption,
    -    entryGroupId ?: username,
    +    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
    +    beginGetCredentialOption = beginGetPublicKeyCredentialOption,
    +    entryGroupId = entryGroupId ?: username,
         isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         affiliatedDomain = affiliatedDomain,
    +    biometricPromptData = biometricPromptData,
     ) {
         val isAutoSelectAllowedFromOption = autoSelectAllowedFromOption
     
    @@ -144,7 +149,7 @@
          * [beginGetPublicKeyCredentialOption] is null
          * @throws IllegalArgumentException if [username] is empty
          */
    -    constructor(
    +    @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
             context: Context,
             username: CharSequence,
             pendingIntent: PendingIntent,
    @@ -154,6 +159,7 @@
             icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
             isAutoSelectAllowed: Boolean = false,
             isDefaultIconPreferredAsSingleProvider: Boolean = false,
    +        biometricPromptData: BiometricPromptData? = null,
         ) : this(
             username,
             displayName,
    @@ -165,7 +171,62 @@
             lastUsedTime,
             isAutoSelectAllowed,
             beginGetPublicKeyCredentialOption,
    -        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider
    +        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
    +        biometricPromptData = biometricPromptData,
    +    )
    +
    +    /**
    +     * @constructor constructs an instance of [PublicKeyCredentialEntry]
    +     *
    +     * @param context the context of the calling app, required to retrieve fallback resources
    +     * @param username the username of the account holding the public key credential
    +     * @param pendingIntent the [PendingIntent] that will get invoked when the user selects this
    +     * entry, must be created with a unique request code per entry,
    +     * with flag [PendingIntent.FLAG_MUTABLE] to allow the Android system to attach the
    +     * final request, and NOT with flag [PendingIntent.FLAG_ONE_SHOT] as it can be invoked multiple
    +     * times
    +     * @param beginGetPublicKeyCredentialOption the option from the original
    +     * [BeginGetCredentialRequest], for which this credential entry is being added
    +     * @param displayName the displayName of the account holding the public key credential
    +     * @param lastUsedTime the last used time the credential underlying this entry was
    +     * used by the user, distinguishable up to the milli second mark only such that if two
    +     * entries have the same millisecond precision, they will be considered to have been used at
    +     * the same time
    +     * @param icon the icon to be displayed with this entry on the selector, if not set, a
    +     * default icon representing a public key credential type is set by the library
    +     * @param isAutoSelectAllowed whether this entry is allowed to be auto
    +     * selected if it is the only one on the UI, only takes effect if the app requesting for
    +     * credentials also opts for auto select
    +     * @param isDefaultIconPreferredAsSingleProvider when set to true, the UI prefers to render the
    +     * default credential type icon (see the default value of [icon]) when you are the
    +     * only available provider; false by default
    +     *
    +     * @throws NullPointerException If [context], [username], [pendingIntent], or
    +     * [beginGetPublicKeyCredentialOption] is null
    +     * @throws IllegalArgumentException if [username] is empty
    +     */
    +    constructor(
    +        context: Context,
    +        username: CharSequence,
    +        pendingIntent: PendingIntent,
    +        beginGetPublicKeyCredentialOption: BeginGetPublicKeyCredentialOption,
    +        displayName: CharSequence? = null,
    +        lastUsedTime: Instant? = null,
    +        icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
    +        isAutoSelectAllowed: Boolean = false,
    +        isDefaultIconPreferredAsSingleProvider: Boolean = false
    +    ) : this(
    +        username = username,
    +        displayName = displayName,
    +        typeDisplayName = context.getString(
    +            R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL
    +        ),
    +        pendingIntent = pendingIntent,
    +        icon = icon,
    +        lastUsedTime = lastUsedTime,
    +        isAutoSelectAllowed = isAutoSelectAllowed,
    +        beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
    +        isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
         )
     
         /**
    @@ -195,7 +256,7 @@
          * [beginGetPublicKeyCredentialOption] is null
          * @throws IllegalArgumentException if [username] is empty
          */
    -    @Deprecated("Use the constructor that allows setting all parameters.",
    +    @Deprecated("Use the constructor with all parameters dependent on API levels",
             replaceWith = ReplaceWith("PublicKeyCredentialEntry(context, username, pendingIntent," +
                 "beginGetPublicKeyCredentialOption, displayName, lastUsedTime, icon, " +
                 "isAutoSelectAllowed, isDefaultIconPreferredAsSingleProvider)"),
    @@ -211,16 +272,16 @@
             icon: Icon = Icon.createWithResource(context, R.drawable.ic_passkey),
             isAutoSelectAllowed: Boolean = false,
         ) : this(
    -        username,
    -        displayName,
    -        context.getString(
    +        username = username,
    +        displayName = displayName,
    +        typeDisplayName = context.getString(
                 R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL
             ),
    -        pendingIntent,
    -        icon,
    -        lastUsedTime,
    -        isAutoSelectAllowed,
    -        beginGetPublicKeyCredentialOption,
    +        pendingIntent = pendingIntent,
    +        icon = icon,
    +        lastUsedTime = lastUsedTime,
    +        isAutoSelectAllowed = isAutoSelectAllowed,
    +        beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
             isDefaultIconPreferredAsSingleProvider = false
         )
     
    @@ -350,6 +411,24 @@
                         .build(),
                     /*subType=*/null
                 )
    +
    +            val biometricPromptData = entry.biometricPromptData
    +            if (biometricPromptData != null) {
    +                val allowedAuthenticators = biometricPromptData.allowedAuthenticators
    +                // TODO(b/326243730) : Await biometric team dependency for opId, then add
    +                val cryptoObjectOpId = biometricPromptData.cryptoObject?.hashCode()
    +
    +                sliceBuilder.addInt(
    +                    allowedAuthenticators, /*subType=*/null,
    +                    listOf(SLICE_HINT_ALLOWED_AUTHENTICATORS)
    +                )
    +                if (cryptoObjectOpId != null) {
    +                    sliceBuilder.addInt(
    +                        cryptoObjectOpId, /*subType=*/null,
    +                        listOf(SLICE_HINT_CRYPTO_OP_ID)
    +                    )
    +                }
    +            }
                 return sliceBuilder.build()
             }
     
    @@ -375,6 +454,7 @@
                 var isDefaultIcon = false
                 var entryGroupId: CharSequence? = null
                 var affiliatedDomain: CharSequence? = null
    +            var allowedAuth: Int? = null
     
                 slice.items.forEach {
                     if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
    @@ -409,9 +489,19 @@
                         entryGroupId = it.text
                     } else if (it.hasHint(SLICE_HINT_AFFILIATED_DOMAIN)) {
                         affiliatedDomain = it.text
    +                } else if (it.hasHint(SLICE_HINT_ALLOWED_AUTHENTICATORS)) {
    +                    allowedAuth = it.int
                     }
                 }
     
    +            // TODO(b/326243730) : Await biometric team dependency for opId, then add - also decide
    +            // if we want toBundle to be passed into the framework.
    +            var biometricPromptDataBundle: Bundle? = null
    +            if (allowedAuth != null) {
    +                biometricPromptDataBundle = Bundle()
    +                biometricPromptDataBundle.putInt(BUNDLE_HINT_ALLOWED_AUTHENTICATORS, allowedAuth!!)
    +            }
    +
                 return try {
                     PublicKeyCredentialEntry(
                         username = title!!,
    @@ -432,6 +522,8 @@
                         autoSelectAllowedFromOption = autoSelectAllowedFromOption,
                         isCreatedFromSlice = true,
                         isDefaultIconFromSlice = isDefaultIcon,
    +                    biometricPromptData = if (biometricPromptDataBundle != null)
    +                        BiometricPromptData.fromBundle(biometricPromptDataBundle) else null
                     )
                 } catch (e: Exception) {
                     Log.i(TAG, "fromSlice failed with: " + e.message)
    @@ -482,6 +574,12 @@
             private const val SLICE_HINT_DEDUPLICATION_ID =
                 "androidx.credentials.provider.credentialEntry.SLICE_HINT_DEDUPLICATION_ID"
     
    +        private const val SLICE_HINT_ALLOWED_AUTHENTICATORS =
    +            "androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS"
    +
    +        private const val SLICE_HINT_CRYPTO_OP_ID =
    +            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID"
    +
             private const val TRUE_STRING = "true"
     
             private const val FALSE_STRING = "false"
    @@ -555,6 +653,7 @@
             private var icon: Icon? = null
             private var autoSelectAllowed: Boolean = false
             private var isDefaultIconPreferredAsSingleProvider: Boolean = false
    +        private var biometricPromptData: BiometricPromptData? = null
     
             /** Sets a displayName to be shown on the UI with this entry */
             fun setDisplayName(displayName: CharSequence?): Builder {
    @@ -569,6 +668,17 @@
             }
     
             /**
    +         * Sets the biometric prompt data to optionally utilize a credential
    +         * manager flow that directly handles the biometric verification for you and gives you the
    +         * response; set to null by default.
    +         */
    +        @RestrictTo(RestrictTo.Scope.LIBRARY)
    +        fun setBiometricPromptData(biometricPromptData: BiometricPromptData): Builder {
    +            this.biometricPromptData = biometricPromptData
    +            return this
    +        }
    +
    +        /**
              * Sets whether the entry should be auto-selected.
              * The value is false by default
              */
    @@ -608,15 +718,16 @@
                     R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL
                 )
                 return PublicKeyCredentialEntry(
    -                username,
    -                displayName,
    -                typeDisplayName,
    -                pendingIntent,
    -                icon!!,
    -                lastUsedTime,
    -                autoSelectAllowed,
    -                beginGetPublicKeyCredentialOption,
    -                isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider
    +                username = username,
    +                displayName = displayName,
    +                typeDisplayName = typeDisplayName,
    +                pendingIntent = pendingIntent,
    +                icon = icon!!,
    +                lastUsedTime = lastUsedTime,
    +                isAutoSelectAllowed = autoSelectAllowed,
    +                beginGetPublicKeyCredentialOption = beginGetPublicKeyCredentialOption,
    +                isDefaultIconPreferredAsSingleProvider = isDefaultIconPreferredAsSingleProvider,
    +                biometricPromptData = biometricPromptData
                 )
             }
         }
    
    diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
    index 5957ac2..4e37e54 100644
    --- a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
    +++ b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
    
    @@ -13,6 +13,8 @@
      * See the License for the specific language governing permissions and
      * limitations under the License.
      */
    +@file:Suppress("deprecation") // For usage of Slice
    +
     package androidx.credentials.provider
     
     import android.annotation.SuppressLint
    
    diff --git a/credentials/credentials/src/main/res/values-hy/strings.xml b/credentials/credentials/src/main/res/values-hy/strings.xml
    index 617300a0..2dfb774f 100644
    --- a/credentials/credentials/src/main/res/values-hy/strings.xml
    +++ b/credentials/credentials/src/main/res/values-hy/strings.xml
    
    @@ -17,6 +17,6 @@
     
     
         xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    -    "Անցաբառ"
    +    "Մուտքի բանալի"
         "Գաղտնաբառ"
     
    
    diff --git a/credentials/credentials/src/main/res/values-nb/strings.xml b/credentials/credentials/src/main/res/values-nb/strings.xml
    index a72318a..9eb70fe 100644
    --- a/credentials/credentials/src/main/res/values-nb/strings.xml
    +++ b/credentials/credentials/src/main/res/values-nb/strings.xml
    
    @@ -17,6 +17,6 @@
     
     
         xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    -    "Tilgangsnøkkel"
    +    "Passnøkkel"
         "Passord"
     
    
    diff --git a/credentials/credentials/src/main/res/values-tr/strings.xml b/credentials/credentials/src/main/res/values-tr/strings.xml
    index f00b298..02256b8 100644
    --- a/credentials/credentials/src/main/res/values-tr/strings.xml
    +++ b/credentials/credentials/src/main/res/values-tr/strings.xml
    
    @@ -17,6 +17,6 @@
     
     
         xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    -    "Şifre anahtarı"
    +    "Geçiş anahtarı"
         "Şifre"
     
    
    diff --git a/credentials/credentials/src/main/res/values-uz/strings.xml b/credentials/credentials/src/main/res/values-uz/strings.xml
    index 7f1bb8c..77100e0 100644
    --- a/credentials/credentials/src/main/res/values-uz/strings.xml
    +++ b/credentials/credentials/src/main/res/values-uz/strings.xml
    
    @@ -17,6 +17,6 @@
     
     
         xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    -    "Kod"
    +    "Kirish kaliti"
         "Parol"
     
    
    diff --git a/credentials/credentials/src/main/res/values/ids.xml b/credentials/credentials/src/main/res/values/ids.xml
    new file mode 100644
    index 0000000..3aac762
    --- /dev/null
    +++ b/credentials/credentials/src/main/res/values/ids.xml
    
    @@ -0,0 +1,4 @@
    +
    +
    +    
    +
    \ No newline at end of file
    
    diff --git a/development/studio/idea.properties b/development/studio/idea.properties
    index 6f9146c..3cabbbf 100644
    --- a/development/studio/idea.properties
    +++ b/development/studio/idea.properties
    
    @@ -5,12 +5,12 @@
     #---------------------------------------------------------------------
     # Uncomment this option if you want to customize path to IDE config folder. Make sure you're using forward slashes.
     #---------------------------------------------------------------------
    -idea.config.path=${user.home}/.AndroidStudioAndroidX/config
    +idea.config.path=${user.home}/.AndroidStudioAndroidXPlatform/config
     
     #---------------------------------------------------------------------
     # Uncomment this option if you want to customize path to IDE system folder. Make sure you're using forward slashes.
     #---------------------------------------------------------------------
    -idea.system.path=${user.home}/.AndroidStudioAndroidX/system
    +idea.system.path=${user.home}/.AndroidStudioAndroidXPlatform/system
     
     #---------------------------------------------------------------------
     # Uncomment this option if you want to customize path to user installed plugins folder. Make sure you're using forward slashes.
    @@ -194,4 +194,4 @@
     #-----------------------------------------------------------------------
     # Enable compose @Preview rendering
     #-----------------------------------------------------------------------
    -compose.project.uses.compose.override=true
    +compose.project.uses.compose.override=true
    \ No newline at end of file
    
    diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
    index 2ce59c4..e4acb72 100644
    --- a/docs-tip-of-tree/build.gradle
    +++ b/docs-tip-of-tree/build.gradle
    
    @@ -21,6 +21,7 @@
         samples("androidx.window:window-samples:1.3.0-alpha03")
     
         docsForOptionalProject(":xr:xr")
    +    docsForOptionalProject(":xr:xr-material3-adaptive")
         docs(project(":activity:activity"))
         docs(project(":activity:activity-compose"))
         docs(project(":activity:activity-ktx"))
    
    diff --git a/gradle.properties b/gradle.properties
    index b5a9b11..2582c25 100644
    --- a/gradle.properties
    +++ b/gradle.properties
    
    @@ -30,11 +30,11 @@
     # Remove when AGP defaults to 2.1.0
     android.prefabVersion=2.1.0
     
    -# Do generate versioned API files
    -androidx.writeVersionedApiFiles=true
    +# Don't generate versioned API files
    +androidx.writeVersionedApiFiles=false
     
    -# Do run the CheckAarMetadata task
    -android.experimental.disableCompileSdkChecks=false
    +# Don't run the CheckAarMetadata task
    +android.experimental.disableCompileSdkChecks=true
     
     # Don't warn about needing to update AGP
     android.suppressUnsupportedCompileSdk=UpsideDownCake,VanillaIceCream,33,34
    @@ -42,7 +42,7 @@
     androidx.compileSdk=34
     androidx.targetSdkVersion=34
     androidx.allowCustomCompileSdk=true
    -androidx.includeOptionalProjects=false
    +androidx.includeOptionalProjects=true
     
     # Keep ComposeCompiler pinned unless performing Kotlin upgrade & ComposeCompiler release
     androidx.unpinComposeCompiler=false
    
    diff --git a/libraryversions.toml b/libraryversions.toml
    index 385c6f9..46d5e1c 100644
    --- a/libraryversions.toml
    +++ b/libraryversions.toml
    
    @@ -8,7 +8,7 @@
     ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
     AUTOFILL = "1.3.0-alpha02"
     BENCHMARK = "1.3.0-alpha05"
    -BIOMETRIC = "1.2.0-alpha06"
    +BIOMETRIC = "1.4.0-alpha01"
     BLUETOOTH = "1.0.0-alpha02"
     BROWSER = "1.9.0-alpha01"
     BUILDSRC_TESTS = "1.0.0-alpha01"
    @@ -31,7 +31,7 @@
     CONSTRAINTLAYOUT_CORE = "1.1.0-alpha13"
     CONTENTPAGER = "1.1.0-alpha01"
     COORDINATORLAYOUT = "1.3.0-alpha02"
    -CORE = "1.14.0-alpha01"
    +CORE = "1.15.0-alpha01"
     CORE_ANIMATION = "1.0.0"
     CORE_ANIMATION_TESTING = "1.0.0"
     CORE_APPDIGEST = "1.0.0-alpha01"
    @@ -45,7 +45,7 @@
     CORE_SPLASHSCREEN = "1.2.0-alpha01"
     CORE_TELECOM = "1.0.0-alpha07"
     CORE_UWB = "1.0.0-alpha08"
    -CREDENTIALS = "1.3.0-beta01"
    +CREDENTIALS = "1.5.0-alpha01"
     CREDENTIALS_E2EE_QUARANTINE = "1.0.0-alpha02"
     CREDENTIALS_FIDO_QUARANTINE = "1.0.0-alpha02"
     CURSORADAPTER = "1.1.0-alpha01"
    @@ -169,8 +169,8 @@
     WEAR_WATCHFACE = "1.3.0-alpha03"
     WEBKIT = "1.12.0-alpha02"
     # Adding a comment to prevent merge conflicts for Window artifact
    -WINDOW = "1.4.0-alpha01"
    -WINDOW_EXTENSIONS = "1.4.0-alpha01"
    +WINDOW = "1.6.0-alpha01"
    +WINDOW_EXTENSIONS = "1.6.0-alpha01"
     WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
     WINDOW_SIDECAR = "1.0.0-rc01"
     WORK = "2.10.0-alpha02"
    
    diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
    index ec6a61b..1ab6667 100644
    --- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
    +++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteChooserDialog.java
    
    @@ -596,4 +596,4 @@
                 }
             }
         }
    -}
    \ No newline at end of file
    +}
    
    diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
    index 989153e..067767a 100644
    --- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
    +++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
    
    @@ -406,7 +406,6 @@
         }
     
         private class TransferCallback extends MediaRouter2.TransferCallback {
    -        TransferCallback() {}
     
             @Override
             public void onTransfer(@NonNull MediaRouter2.RoutingController oldController,
    
    diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
    index fda546c..d9c0c48 100644
    --- a/recyclerview/recyclerview/build.gradle
    +++ b/recyclerview/recyclerview/build.gradle
    
    @@ -57,6 +57,7 @@
         }
     
         defaultConfig {
    +        compileSdkPreview "VanillaIceCream"
             multiDexEnabled = true
             testInstrumentationRunner "androidx.recyclerview.test.TestRunner"
             multiDexEnabled true
    
    diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
    new file mode 100644
    index 0000000..5b4c2f7
    --- /dev/null
    +++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewScrollFrameRateTest.kt
    
    @@ -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.
    + */
    +@file:Suppress("DEPRECATION")
    +
    +package androidx.recyclerview.widget
    +
    +import android.graphics.Color
    +import android.os.Build
    +import android.view.View
    +import android.view.ViewGroup
    +import android.view.ViewTreeObserver
    +import android.widget.TextView
    +import androidx.core.os.BuildCompat
    +import androidx.test.ext.junit.runners.AndroidJUnit4
    +import androidx.test.filters.MediumTest
    +import androidx.test.filters.SdkSuppress
    +import androidx.test.rule.ActivityTestRule
    +import com.google.common.truth.Truth.assertThat
    +import java.util.concurrent.CountDownLatch
    +import java.util.concurrent.TimeUnit
    +import org.junit.Rule
    +import org.junit.Test
    +import org.junit.runner.RunWith
    +
    +@MediumTest
    +// TODO: change to VANILLA_ICE_CREAM when it is ready
    +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +@RunWith(AndroidJUnit4::class)
    +class RecyclerViewScrollFrameRateTest {
    +    @get:Rule
    +    val rule = ActivityTestRule(TestContentViewActivity::class.java)
    +
    +    @Test
    +    fun smoothScrollFrameRateBoost() {
    +        // TODO: Remove when VANILLA_ICE_CREAM is ready and the SdkSuppress is modified
    +        if (!BuildCompat.isAtLeastV()) {
    +            return
    +        }
    +        val rv = RecyclerView(rule.activity)
    +        rule.runOnUiThread {
    +            rv.layoutManager =
    +                LinearLayoutManager(rule.activity, LinearLayoutManager.VERTICAL, false)
    +            rv.adapter = object : RecyclerView.Adapter() {
    +                override fun onCreateViewHolder(
    +                    parent: ViewGroup,
    +                    viewType: Int
    +                ): RecyclerView.ViewHolder {
    +                    val view = TextView(parent.context)
    +                    view.textSize = 40f
    +                    view.setTextColor(Color.WHITE)
    +                    return object : RecyclerView.ViewHolder(view) {}
    +                }
    +
    +                override fun getItemCount(): Int = 10000
    +
    +                override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    +                    val view = holder.itemView as TextView
    +                    view.text = "Text $position"
    +                    val color = if (position % 2 == 0) Color.BLACK else 0xFF000080.toInt()
    +                    view.setBackgroundColor(color)
    +                }
    +            }
    +            rule.activity.contentView.addView(rv)
    +        }
    +        runOnDraw(rv, { rv.smoothScrollBy(0, 1000) }) {
    +            // First Frame
    +            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
    +        }
    +
    +        // Second frame
    +        runOnDraw(rv) {
    +            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
    +        }
    +
    +        // Third frame
    +        runOnDraw(rv) {
    +            assertThat(rv.frameContentVelocity).isGreaterThan(0f)
    +        }
    +    }
    +
    +    private fun runOnDraw(view: View, setup: () -> Unit = { }, onDraw: () -> Unit) {
    +        val latch = CountDownLatch(1)
    +        val onDrawListener = ViewTreeObserver.OnDrawListener {
    +            latch.countDown()
    +            onDraw()
    +        }
    +        rule.runOnUiThread {
    +            view.viewTreeObserver.addOnDrawListener(onDrawListener)
    +            setup()
    +        }
    +        assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
    +        rule.runOnUiThread {
    +            view.viewTreeObserver.removeOnDrawListener(onDrawListener)
    +        }
    +    }
    +}
    
    diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
    index 80be2f0..0d47108 100644
    --- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
    +++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
    
    @@ -64,12 +64,15 @@
     import android.widget.OverScroller;
     
     import androidx.annotation.CallSuper;
    +import androidx.annotation.DoNotInline;
     import androidx.annotation.IntDef;
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.Px;
    +import androidx.annotation.RequiresApi;
     import androidx.annotation.RestrictTo;
     import androidx.annotation.VisibleForTesting;
    +import androidx.core.os.BuildCompat;
     import androidx.core.os.TraceCompat;
     import androidx.core.util.Preconditions;
     import androidx.core.view.AccessibilityDelegateCompat;
    @@ -6005,6 +6008,10 @@
                             mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
                         }
                     }
    +                if (BuildCompat.isAtLeastV()) {
    +                    Api35Impl.setFrameContentVelocity(RecyclerView.this,
    +                            Math.abs(scroller.getCurrVelocity()));
    +                }
                 }
     
                 SmoothScroller smoothScroller = mLayout.mSmoothScroller;
    @@ -14627,4 +14634,12 @@
             }
             return mScrollingChildHelper;
         }
    +
    +    @RequiresApi(35)
    +    private static final class Api35Impl {
    +        @DoNotInline
    +        public static void setFrameContentVelocity(View view, float velocity) {
    +            view.setFrameContentVelocity(velocity);
    +        }
    +    }
     }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
    index aed9f13..cb1f13b 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/AddEditRouteActivity.java
    
    @@ -44,7 +44,7 @@
     public class AddEditRouteActivity extends AppCompatActivity {
         private static final String EXTRA_ROUTE_ID_KEY = "routeId";
     
    -    private SampleDynamicGroupMediaRouteProviderService mService;
    +    @Nullable private SampleDynamicGroupMediaRouteProviderService mService;
         private ServiceConnection mConnection;
         private RoutesManager mRoutesManager;
         private RouteItem mRouteItem;
    @@ -163,7 +163,9 @@
             saveButton.setOnClickListener(
                     view -> {
                         mRoutesManager.addRoute(mRouteItem);
    -                    mService.reloadRoutes();
    +                    if (mService != null) {
    +                        mService.reloadRoutes();
    +                    }
                         finish();
                     });
         }
    @@ -203,7 +205,7 @@
             }
     
             @Override
    -        public void onServiceDisconnected(ComponentName arg0) {
    +        public void onServiceDisconnected(ComponentName unusedComponentName) {
                 mService = null;
             }
         }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
    index e360241..508d86d 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
    
    @@ -649,6 +649,9 @@
             @Override
             public void onRouteChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) {
                 Log.d(TAG, "onRouteChanged: route=" + route);
    +            if (route.isSelected()) {
    +                updateRouteDescription();
    +            }
             }
     
             @Override
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
    index 2db33b8..1285855 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/SettingsActivity.java
    
    @@ -21,6 +21,7 @@
     import android.content.Context;
     import android.content.Intent;
     import android.content.ServiceConnection;
    +import android.content.pm.PackageManager;
     import android.os.Bundle;
     import android.os.IBinder;
     import android.view.View;
    @@ -43,6 +44,7 @@
     import com.example.androidx.mediarouting.RoutesManager;
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutingActivity;
     import com.example.androidx.mediarouting.services.SampleDynamicGroupMediaRouteProviderService;
    +import com.example.androidx.mediarouting.services.SampleMediaRouteProviderService;
     import com.example.androidx.mediarouting.ui.RoutesAdapter;
     import com.google.android.material.floatingactionbutton.FloatingActionButton;
     
    @@ -52,20 +54,20 @@
      * SampleDynamicGroupMediaRouteProviderService}.
      */
     public final class SettingsActivity extends AppCompatActivity {
    +    private final ProviderServiceConnection mConnection = new ProviderServiceConnection();
    +    private PackageManager mPackageManager;
         private MediaRouter mMediaRouter;
         private RoutesManager mRoutesManager;
         private RoutesAdapter mRoutesAdapter;
    -    private SampleDynamicGroupMediaRouteProviderService mService;
    -    private ServiceConnection mConnection;
     
         @Override
         protected void onCreate(@Nullable Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_settings);
     
    +        mPackageManager = getPackageManager();
             mMediaRouter = MediaRouter.getInstance(this);
             mRoutesManager = RoutesManager.getInstance(getApplicationContext());
    -        mConnection = new ProviderServiceConnection();
     
             setUpViews();
     
    @@ -86,7 +88,11 @@
                                     android.R.string.ok,
                                     (dialogInterface, i) -> {
                                         mRoutesManager.deleteRouteWithId(routeId);
    -                                    mService.reloadRoutes();
    +                                    SampleDynamicGroupMediaRouteProviderService providerService =
    +                                            mConnection.mService;
    +                                    if (providerService != null) {
    +                                        providerService.reloadRoutes();
    +                                    }
                                         mRoutesAdapter.updateRoutes(
                                                 mRoutesManager.getRouteItems());
                                     })
    @@ -111,10 +117,7 @@
         @Override
         protected void onStart() {
             super.onStart();
    -        // Bind to SampleDynamicGroupMediaRouteProviderService
    -        Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
    -        intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
    -        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    +        bindToDynamicProviderService();
         }
     
         @Override
    @@ -125,13 +128,20 @@
     
         @Override
         protected void onStop() {
    +        try {
    +            unbindService(mConnection);
    +        } catch (RuntimeException e) {
    +            // This happens when the provider is disabled, but there's no way of preventing this
    +            // completely so we just ignore the exception.
    +        }
             super.onStop();
    -        unbindService(mConnection);
         }
     
         private void setUpViews() {
             setUpDynamicGroupsEnabledSwitch();
             setUpTransferToLocalSwitch();
    +        setUpSimpleProviderEnabledSwitch();
    +        setUpDynamicProviderEnabledSwitch();
             setUpDialogTypeDropDownList();
             setUpNewRouteButton();
             setupSystemRoutesButton();
    @@ -141,9 +151,13 @@
             Switch dynamicRoutingEnabled = findViewById(R.id.dynamic_routing_switch);
             dynamicRoutingEnabled.setChecked(mRoutesManager.isDynamicRoutingEnabled());
             dynamicRoutingEnabled.setOnCheckedChangeListener(
    -                (compoundButton, b) -> {
    -                    mRoutesManager.setDynamicRoutingEnabled(b);
    -                    mService.reloadDynamicRoutesEnabled();
    +                (compoundButton, enabled) -> {
    +                    mRoutesManager.setDynamicRoutingEnabled(enabled);
    +                    SampleDynamicGroupMediaRouteProviderService providerService =
    +                            mConnection.mService;
    +                    if (providerService != null) {
    +                        providerService.reloadDynamicRoutesEnabled();
    +                    }
                     });
         }
     
    @@ -151,14 +165,56 @@
             Switch showThisPhoneSwitch = findViewById(R.id.show_this_phone_switch);
             showThisPhoneSwitch.setChecked(mMediaRouter.getRouterParams().isTransferToLocalEnabled());
             showThisPhoneSwitch.setOnCheckedChangeListener(
    -                (compoundButton, b) -> {
    +                (compoundButton, enabled) -> {
                         MediaRouterParams.Builder builder =
                                 new MediaRouterParams.Builder(mMediaRouter.getRouterParams());
    -                    builder.setTransferToLocalEnabled(b);
    +                    builder.setTransferToLocalEnabled(enabled);
                         mMediaRouter.setRouterParams(builder.build());
                     });
         }
     
    +    private void setUpSimpleProviderEnabledSwitch() {
    +        Switch simpleProviderEnabledSwitch = findViewById(R.id.enable_simple_provider_switch);
    +        ComponentName simpleProviderComponentName =
    +                new ComponentName(/* context= */ this, SampleMediaRouteProviderService.class);
    +        simpleProviderEnabledSwitch.setChecked(
    +                mPackageManager.getComponentEnabledSetting(simpleProviderComponentName)
    +                        != PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
    +        simpleProviderEnabledSwitch.setOnCheckedChangeListener(
    +                (compoundButton, enabled) -> {
    +                    mPackageManager
    +                            .setComponentEnabledSetting(
    +                                    simpleProviderComponentName,
    +                                    enabled
    +                                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
    +                                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
    +                                    /* flags= */ PackageManager.DONT_KILL_APP);
    +                });
    +    }
    +
    +    private void setUpDynamicProviderEnabledSwitch() {
    +        Switch dynamicProviderEnabledSwitch = findViewById(R.id.enable_dynamic_provider_switch);
    +        ComponentName dynamicProviderComponentName =
    +                new ComponentName(
    +                        /* context= */ this, SampleDynamicGroupMediaRouteProviderService.class);
    +        dynamicProviderEnabledSwitch.setChecked(
    +                mPackageManager.getComponentEnabledSetting(dynamicProviderComponentName)
    +                        != PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
    +        dynamicProviderEnabledSwitch.setOnCheckedChangeListener(
    +                (compoundButton, enabled) -> {
    +                    mPackageManager
    +                            .setComponentEnabledSetting(
    +                                    dynamicProviderComponentName,
    +                                    enabled
    +                                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
    +                                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
    +                                    /* flags= */ PackageManager.DONT_KILL_APP);
    +                    if (enabled) {
    +                        bindToDynamicProviderService();
    +                    }
    +                });
    +    }
    +
         private void setUpDialogTypeDropDownList() {
             Spinner spinner = findViewById(R.id.dialog_spinner);
             spinner.setOnItemSelectedListener(
    @@ -204,13 +260,22 @@
             showSystemRoutesButton.setOnClickListener(v -> SystemRoutingActivity.launch(this));
         }
     
    -    private class ProviderServiceConnection implements ServiceConnection {
    +    private void bindToDynamicProviderService() {
    +        Intent intent = new Intent(this, SampleDynamicGroupMediaRouteProviderService.class);
    +        intent.setAction(SampleDynamicGroupMediaRouteProviderService.ACTION_BIND_LOCAL);
    +        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    +    }
    +
    +    private static class ProviderServiceConnection implements ServiceConnection {
    +
    +        @Nullable private SampleDynamicGroupMediaRouteProviderService mService;
     
             @Override
             public void onServiceConnected(ComponentName className, IBinder service) {
                 SampleDynamicGroupMediaRouteProviderService.LocalBinder binder =
                         (SampleDynamicGroupMediaRouteProviderService.LocalBinder) service;
                 mService = binder.getService();
    +            mService.reloadRoutes();
             }
     
             @Override
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
    index c92ced8..0f331cd 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRouteItem.java
    
    @@ -19,71 +19,66 @@
     import android.text.TextUtils;
     
     import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +
    +import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
     
     import java.util.Objects;
     
    -import javax.annotation.Nullable;
    -
    -/**
    - * An abstract model that holds information about routes from different sources.
    - *
    - * Can represent media routers' routes, bluetooth routes, or audio routes.
    - */
    +/** Holds information about a system route. */
     public final class SystemRouteItem implements SystemRoutesAdapterItem {
     
    -    @NonNull
    -    private final String mId;
    +    /**
    +     * Describes the support of a route for selection.
    +     *
    +     * 

    We understand by selection the action that makes a specific route the active route. Note + * that this terminology may not match the terminology used by the underlying {@link + * SystemRoutesSource}. + */ + public enum SelectionSupportState { + /** The underlying route source doesn't support selection. */ + UNSUPPORTED, + /** + * The corresponding route is already selected, but can be reselected. + * + *

    Selecting an already selected route (reselection) can change the metadata of the route + * source. For example, reselecting a MediaRouter2 route can alter the transfer reason. + */ + RESELECTABLE, + /** The route is available for selection. */ + SELECTABLE + } - @NonNull - private final String mName; + /** The {@link SystemRoutesSource#getSourceId()} of the source that created this item. */ + @NonNull public final String mSourceId; - @Nullable - private final String mAddress; + /** An id that uniquely identifies this route item within the source. */ + @NonNull public final String mId; - @Nullable - private final String mDescription; + @NonNull public final String mName; + + @Nullable public final String mAddress; + + @Nullable public final String mDescription; + + @Nullable public final String mSuitabilityStatus; + + @Nullable public final Boolean mTransferInitiatedBySelf; + + @Nullable public final String mTransferReason; + + @NonNull public final SelectionSupportState mSelectionSupportState; private SystemRouteItem(@NonNull Builder builder) { - Objects.requireNonNull(builder.mId); - Objects.requireNonNull(builder.mName); - - mId = builder.mId; - mName = builder.mName; - + mSourceId = Objects.requireNonNull(builder.mSourceId); + mId = Objects.requireNonNull(builder.mId); + mName = Objects.requireNonNull(builder.mName); mAddress = builder.mAddress; mDescription = builder.mDescription; - } - - /** - * Returns a unique identifier of a route. - */ - @NonNull - public String getId() { - return mId; - } - - /** - * Returns a human-readable name of the route. - */ - @NonNull - public String getName() { - return mName; - } - - /** - * Returns address if the route is a Bluetooth route and {@code null} otherwise. - */ - @Nullable - public String getAddress() { - return mAddress; - } - - /** - * Returns a route description or {@code null} if empty. - */ - @Nullable - public String getDescription() { - return mDescription; + mSuitabilityStatus = builder.mSuitabilityStatus; + mTransferInitiatedBySelf = builder.mTransferInitiatedBySelf; + mTransferReason = builder.mTransferReason; + mSelectionSupportState = builder.mSelectionSupportState; } @Override @@ -91,14 +86,29 @@ if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SystemRouteItem that = (SystemRouteItem) o; - return mId.equals(that.mId) && mName.equals(that.mName) - && Objects.equals(mAddress, that.mAddress) && Objects.equals( - mDescription, that.mDescription); + return mSourceId.equals(that.mSourceId) + && mId.equals(that.mId) + && mName.equals(that.mName) + && Objects.equals(mAddress, that.mAddress) + && Objects.equals(mDescription, that.mDescription) + && Objects.equals(mSuitabilityStatus, that.mSuitabilityStatus) + && Objects.equals(mTransferInitiatedBySelf, that.mTransferInitiatedBySelf) + && Objects.equals(mTransferReason, that.mTransferReason) + && mSelectionSupportState.equals(that.mSelectionSupportState); } @Override public int hashCode() { - return Objects.hash(mId, mName, mAddress, mDescription); + return Objects.hash( + mSourceId, + mId, + mName, + mAddress, + mDescription, + mSuitabilityStatus, + mTransferInitiatedBySelf, + mTransferReason, + mSelectionSupportState); } /** @@ -106,20 +116,26 @@ */ public static final class Builder { - @NonNull - private final String mId; + @NonNull private String mSourceId; + @NonNull private final String mId; + @NonNull private String mName; + @Nullable private String mAddress; + @Nullable private String mDescription; + @Nullable private String mSuitabilityStatus; + @Nullable private Boolean mTransferInitiatedBySelf; + @Nullable private String mTransferReason; + @NonNull public SelectionSupportState mSelectionSupportState; - @NonNull - private String mName; - - @Nullable - private String mAddress; - - @Nullable - private String mDescription; - - public Builder(@NonNull String id) { + /** + * Creates a builder with the mandatory properties. + * + * @param sourceId See {@link SystemRouteItem#mSourceId}. + * @param id See {@link SystemRouteItem#mId}. + */ + public Builder(@NonNull String sourceId, @NonNull String id) { + mSourceId = sourceId; mId = id; + mSelectionSupportState = SelectionSupportState.UNSUPPORTED; } /** @@ -154,6 +170,43 @@ } /** + * Sets a human-readable string describing the transfer suitability of the route, or null if + * not applicable. + */ + @NonNull + public Builder setSuitabilityStatus(@Nullable String suitabilityStatus) { + mSuitabilityStatus = suitabilityStatus; + return this; + } + + /** + * Sets whether the corresponding route's selection is the result of an action of this app, + * or null if not applicable. + */ + @NonNull + public Builder setTransferInitiatedBySelf(@Nullable Boolean transferInitiatedBySelf) { + mTransferInitiatedBySelf = transferInitiatedBySelf; + return this; + } + + /** + * Sets a human-readable string describing the transfer reason, or null if not applicable. + */ + @NonNull + public Builder setTransferReason(@Nullable String transferReason) { + mTransferReason = transferReason; + return this; + } + + /** Sets the {@link SelectionSupportState} for the corresponding route. */ + @NonNull + public Builder setSelectionSupportState( + @NonNull SelectionSupportState selectionSupportState) { + mSelectionSupportState = Objects.requireNonNull(selectionSupportState); + return this; + } + + /** * Builds {@link SystemRouteItem}. */ @NonNull

    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
    index aba49a1..764c050 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutesAdapter.java
    
    @@ -16,14 +16,21 @@
     
     package com.example.androidx.mediarouting.activities.systemrouting;
     
    +import static com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem.SelectionSupportState.SELECTABLE;
    +import static com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem.SelectionSupportState.UNSUPPORTED;
    +
    +import android.annotation.SuppressLint;
     import android.content.Context;
     import android.view.LayoutInflater;
     import android.view.View;
     import android.view.ViewGroup;
    +import android.widget.TextView;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    +import androidx.appcompat.widget.AppCompatButton;
     import androidx.appcompat.widget.AppCompatTextView;
    +import androidx.core.util.Consumer;
     import androidx.recyclerview.widget.AsyncListDiffer;
     import androidx.recyclerview.widget.DiffUtil;
     import androidx.recyclerview.widget.RecyclerView;
    @@ -32,17 +39,19 @@
     
     import java.util.List;
     
    -/**
    - * @link RecyclerView.Adapter} for showing system route sources and the routes discovered by each
    - * source.
    - */
    -class SystemRoutesAdapter extends RecyclerView.Adapter {
    +/** {@link RecyclerView.Adapter} for showing system route sources and their corresponding routes. */
    +/* package */ class SystemRoutesAdapter extends RecyclerView.Adapter {
     
         private static final int VIEW_TYPE_HEADER = 0;
         private static final int VIEW_TYPE_ITEM = 1;
     
         private final AsyncListDiffer mListDiffer =
                 new AsyncListDiffer<>(this, new ItemCallback());
    +    private final Consumer mRouteItemClickedListener;
    +
    +    /* package */ SystemRoutesAdapter(Consumer routeItemClickListener) {
    +        mRouteItemClickedListener = routeItemClickListener;
    +    }
     
         public void setItems(@NonNull List newItems) {
             mListDiffer.submitList(newItems);
    @@ -108,12 +117,16 @@
             }
         }
     
    -    static class ItemViewHolder extends RecyclerView.ViewHolder {
    +    private class ItemViewHolder extends RecyclerView.ViewHolder {
     
             private final AppCompatTextView mRouteNameTextView;
             private final AppCompatTextView mRouteIdTextView;
             private final AppCompatTextView mRouteAddressTextView;
             private final AppCompatTextView mRouteDescriptionTextView;
    +        private final AppCompatTextView mSuitabilityStatusTextView;
    +        private final AppCompatTextView mTransferInitiatedBySelfTextView;
    +        private final AppCompatTextView mTransferReasonTextView;
    +        private final AppCompatButton mSelectionButton;
     
             ItemViewHolder(@NonNull View itemView) {
                 super(itemView);
    @@ -122,20 +135,41 @@
                 mRouteIdTextView = itemView.findViewById(R.id.route_id);
                 mRouteAddressTextView = itemView.findViewById(R.id.route_address);
                 mRouteDescriptionTextView = itemView.findViewById(R.id.route_description);
    +            mSuitabilityStatusTextView = itemView.findViewById(R.id.route_suitability_status);
    +            mTransferInitiatedBySelfTextView =
    +                    itemView.findViewById(R.id.route_transfer_initiated_by_self);
    +            mTransferReasonTextView = itemView.findViewById(R.id.route_transfer_reason);
    +            mSelectionButton = itemView.findViewById(R.id.route_selection_button);
             }
     
             void bind(SystemRouteItem systemRouteItem) {
    -            mRouteNameTextView.setText(systemRouteItem.getName());
    -            mRouteIdTextView.setText(systemRouteItem.getId());
    +            mRouteNameTextView.setText(systemRouteItem.mName);
    +            mRouteIdTextView.setText(systemRouteItem.mId);
    +            setTextOrHide(mRouteAddressTextView, systemRouteItem.mAddress);
    +            setTextOrHide(mRouteDescriptionTextView, systemRouteItem.mDescription);
    +            setTextOrHide(mSuitabilityStatusTextView, systemRouteItem.mSuitabilityStatus);
    +            String initiatedBySelfText =
    +                    systemRouteItem.mTransferInitiatedBySelf != null
    +                            ? "self-initiated: " + systemRouteItem.mTransferInitiatedBySelf
    +                            : null;
    +            setTextOrHide(mTransferInitiatedBySelfTextView, initiatedBySelfText);
    +            String transferReasonText =
    +                    systemRouteItem.mTransferReason != null
    +                            ? "transfer reason: " + systemRouteItem.mTransferReason
    +                            : null;
    +            setTextOrHide(mTransferReasonTextView, transferReasonText);
     
    -            showViewIfNotNull(mRouteAddressTextView, systemRouteItem.getAddress());
    -            if (systemRouteItem.getAddress() != null) {
    -                mRouteAddressTextView.setText(systemRouteItem.getAddress());
    -            }
    -
    -            showViewIfNotNull(mRouteDescriptionTextView, systemRouteItem.getDescription());
    -            if (systemRouteItem.getDescription() != null) {
    -                mRouteDescriptionTextView.setText(systemRouteItem.getDescription());
    +            if (systemRouteItem.mSelectionSupportState == UNSUPPORTED) {
    +                mSelectionButton.setVisibility(View.GONE);
    +            } else {
    +                mSelectionButton.setVisibility(View.VISIBLE);
    +                String text =
    +                        systemRouteItem.mSelectionSupportState == SELECTABLE
    +                                ? "Select"
    +                                : "Reselect";
    +                mSelectionButton.setText(text);
    +                mSelectionButton.setOnClickListener(
    +                        view -> mRouteItemClickedListener.accept(systemRouteItem));
                 }
             }
         }
    @@ -145,8 +179,7 @@
             public boolean areItemsTheSame(@NonNull SystemRoutesAdapterItem oldItem,
                     @NonNull SystemRoutesAdapterItem newItem) {
                 if (oldItem instanceof SystemRouteItem && newItem instanceof SystemRouteItem) {
    -                return ((SystemRouteItem) oldItem).getId().equals(
    -                        ((SystemRouteItem) newItem).getId());
    +                return ((SystemRouteItem) oldItem).mId.equals(((SystemRouteItem) newItem).mId);
                 } else if (oldItem instanceof SystemRoutesSourceItem
                         && newItem instanceof SystemRoutesSourceItem) {
                     return oldItem.equals(newItem);
    @@ -155,25 +188,21 @@
                 }
             }
     
    +        @SuppressLint("DiffUtilEquals")
             @Override
    -        public boolean areContentsTheSame(@NonNull SystemRoutesAdapterItem oldItem,
    +        public boolean areContentsTheSame(
    +                @NonNull SystemRoutesAdapterItem oldItem,
                     @NonNull SystemRoutesAdapterItem newItem) {
    -            if (oldItem instanceof SystemRouteItem && newItem instanceof SystemRouteItem) {
    -                return oldItem.equals(newItem);
    -            } else if (oldItem instanceof SystemRoutesSourceItem
    -                    && newItem instanceof SystemRoutesSourceItem) {
    -                return oldItem.equals(newItem);
    -            } else {
    -                return false;
    -            }
    +            return oldItem.equals(newItem);
             }
         }
     
    -    private static  void showViewIfNotNull(@NonNull V view, @Nullable T obj) {
    -        if (obj == null) {
    +    private static void setTextOrHide(@NonNull TextView view, @Nullable String text) {
    +        if (text == null) {
                 view.setVisibility(View.GONE);
             } else {
                 view.setVisibility(View.VISIBLE);
    +            view.setText(text);
             }
         }
     }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
    index 73d57fe..6f18b57 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/SystemRoutingActivity.java
    
    @@ -44,7 +44,9 @@
     import com.example.androidx.mediarouting.activities.systemrouting.source.SystemRoutesSource;
     
     import java.util.ArrayList;
    +import java.util.HashMap;
     import java.util.List;
    +import java.util.Map;
     
     /**
      * Shows available system routes gathered from different sources.
    @@ -54,15 +56,11 @@
         private static final int REQUEST_CODE_BLUETOOTH_CONNECT = 4199;
     
         @NonNull
    -    private final SystemRoutesAdapter mSystemRoutesAdapter = new SystemRoutesAdapter();
    -    @NonNull
    -    private final List mSystemRoutesSources = new ArrayList<>();
    -    @NonNull
    -    private final SystemRoutesSourceCallback mSystemRoutesSourceCallback =
    -            new SystemRoutesSourceCallback();
    +    private final SystemRoutesAdapter mSystemRoutesAdapter =
    +            new SystemRoutesAdapter(this::onRouteItemClicked);
     
    -    @NonNull
    -    private SwipeRefreshLayout mSwipeRefreshLayout;
    +    @NonNull private final Map mSystemRoutesSources = new HashMap<>();
    +    @NonNull private SwipeRefreshLayout mSwipeRefreshLayout;
     
         /**
          * Creates and launches an intent to start current activity.
    @@ -95,7 +93,7 @@
     
         @Override
         protected void onDestroy() {
    -        for (SystemRoutesSource source: mSystemRoutesSources) {
    +        for (SystemRoutesSource source : mSystemRoutesSources.values()) {
                 source.stop();
             }
     
    @@ -119,7 +117,7 @@
     
         private void refreshSystemRoutesList() {
             List systemRoutesSourceItems = new ArrayList<>();
    -        for (SystemRoutesSource source : mSystemRoutesSources) {
    +        for (SystemRoutesSource source : mSystemRoutesSources.values()) {
                 systemRoutesSourceItems.add(source.getSourceItem());
                 systemRoutesSourceItems.addAll(source.fetchSourceRouteItems());
             }
    @@ -127,6 +125,20 @@
             mSwipeRefreshLayout.setRefreshing(false);
         }
     
    +    private void onRouteItemClicked(SystemRouteItem item) {
    +        SystemRoutesSource systemRoutesSource = mSystemRoutesSources.get(item.mSourceId);
    +        if (systemRoutesSource == null) {
    +            throw new IllegalStateException("Couldn't find source with id: " + item.mSourceId);
    +        }
    +        if (!systemRoutesSource.select(item)) {
    +            Toast.makeText(
    +                            /* context= */ this,
    +                            "Something went wrong with route selection",
    +                            Toast.LENGTH_LONG)
    +                    .show();
    +        }
    +    }
    +
         private boolean hasBluetoothPermission() {
             return ContextCompat.checkSelfPermission(/* context= */ this, BLUETOOTH_CONNECT)
                     == PERMISSION_GRANTED
    @@ -150,40 +162,28 @@
         }
     
         private void initializeSystemRoutesSources() {
    -        mSystemRoutesSources.clear();
    -
    -        mSystemRoutesSources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
    +        ArrayList sources = new ArrayList<>();
    +        sources.add(MediaRouterSystemRoutesSource.create(/* context= */ this));
     
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    -            mSystemRoutesSources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
    +            sources.add(MediaRouter2SystemRoutesSource.create(/* context= */ this));
             }
     
    -        mSystemRoutesSources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
    +        sources.add(AndroidXMediaRouterSystemRoutesSource.create(/* context= */ this));
     
             if (hasBluetoothPermission()) {
    -            mSystemRoutesSources.add(
    -                    BluetoothManagerSystemRoutesSource.create(/* context= */ this));
    +            sources.add(BluetoothManagerSystemRoutesSource.create(/* context= */ this));
             }
     
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    -            mSystemRoutesSources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
    +            sources.add(AudioManagerSystemRoutesSource.create(/* context= */ this));
             }
     
    -        for (SystemRoutesSource source: mSystemRoutesSources) {
    -            source.setOnRoutesChangedListener(mSystemRoutesSourceCallback);
    +        mSystemRoutesSources.clear();
    +        for (SystemRoutesSource source : sources) {
    +            source.setOnRoutesChangedListener(this::refreshSystemRoutesList);
    +            mSystemRoutesSources.put(source.getSourceId(), source);
                 source.start();
             }
         }
    -
    -    private class SystemRoutesSourceCallback implements SystemRoutesSource.OnRoutesChangedListener {
    -        @Override
    -        public void onRouteAdded(@NonNull SystemRouteItem routeItem) {
    -            refreshSystemRoutesList();
    -        }
    -
    -        @Override
    -        public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {
    -            refreshSystemRoutesList();
    -        }
    -    }
     }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
    index 7e553ed..3311c82 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AndroidXMediaRouterSystemRoutesSource.java
    
    @@ -36,19 +36,20 @@
         private final MediaRouter mMediaRouter;
     
         @NonNull
    -    private final MediaRouter.Callback mMediaRouterCallback = new MediaRouter.Callback() {
    -        @Override
    -        public void onRouteAdded(@NonNull MediaRouter router,
    -                @NonNull MediaRouter.RouteInfo route) {
    -            mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(route));
    -        }
    +    private final MediaRouter.Callback mMediaRouterCallback =
    +            new MediaRouter.Callback() {
    +                @Override
    +                public void onRouteAdded(
    +                        @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
    +                    mOnRoutesChangedListener.run();
    +                }
     
    -        @Override
    -        public void onRouteRemoved(@NonNull MediaRouter router,
    -                @NonNull MediaRouter.RouteInfo route) {
    -            mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(route));
    -        }
    -    };
    +                @Override
    +                public void onRouteRemoved(
    +                        @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
    +                    mOnRoutesChangedListener.run();
    +                }
    +            };
     
         /** Returns a new instance. */
         @NonNull
    @@ -98,10 +99,16 @@
             return out;
         }
     
    +    @Override
    +    public boolean select(@NonNull SystemRouteItem item) {
    +        throw new UnsupportedOperationException();
    +    }
    +
         @NonNull
    -    private static SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
    -        SystemRouteItem.Builder builder = new SystemRouteItem.Builder(routeInfo.getId())
    -                .setName(routeInfo.getName());
    +    private SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
    +        SystemRouteItem.Builder builder =
    +                new SystemRouteItem.Builder(getSourceId(), routeInfo.getId())
    +                        .setName(routeInfo.getName());
     
             String description = routeInfo.getDescription();
             if (description != null) {
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
    index 80d6e33..9d2d0fd 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/AudioManagerSystemRoutesSource.java
    
    @@ -40,21 +40,18 @@
         private final AudioManager mAudioManager;
     
         @NonNull
    -    private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallback() {
    -        @Override
    -        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
    -            for (AudioDeviceInfo audioDeviceInfo: addedDevices) {
    -                mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(audioDeviceInfo));
    -            }
    -        }
    +    private final AudioDeviceCallback mAudioDeviceCallback =
    +            new AudioDeviceCallback() {
    +                @Override
    +                public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
    +                    mOnRoutesChangedListener.run();
    +                }
     
    -        @Override
    -        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
    -            for (AudioDeviceInfo audioDeviceInfo: removedDevices) {
    -                mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(audioDeviceInfo));
    -            }
    -        }
    -    };
    +                @Override
    +                public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
    +                    mOnRoutesChangedListener.run();
    +                }
    +            };
     
         /** Returns a new instance. */
         @NonNull
    @@ -96,11 +93,16 @@
             return out;
         }
     
    +    @Override
    +    public boolean select(@NonNull SystemRouteItem item) {
    +        throw new UnsupportedOperationException();
    +    }
    +
         @NonNull
    -    private static SystemRouteItem createRouteItemFor(@NonNull AudioDeviceInfo audioDeviceInfo) {
    -        SystemRouteItem.Builder builder = new SystemRouteItem.Builder(
    -                String.valueOf(audioDeviceInfo.getId()))
    -                .setName(audioDeviceInfo.getProductName().toString());
    +    private SystemRouteItem createRouteItemFor(@NonNull AudioDeviceInfo audioDeviceInfo) {
    +        SystemRouteItem.Builder builder =
    +                new SystemRouteItem.Builder(getSourceId(), String.valueOf(audioDeviceInfo.getId()))
    +                        .setName(audioDeviceInfo.getProductName().toString());
     
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                 builder.setAddress(Api28Impl.getAddress(audioDeviceInfo));
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
    index 96fab5b..7bc34d7 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/BluetoothManagerSystemRoutesSource.java
    
    @@ -23,7 +23,6 @@
     import android.bluetooth.BluetoothHearingAid;
     import android.bluetooth.BluetoothLeAudio;
     import android.bluetooth.BluetoothManager;
    -import android.bluetooth.BluetoothProfile;
     import android.content.BroadcastReceiver;
     import android.content.Context;
     import android.content.Intent;
    @@ -31,7 +30,6 @@
     
     import androidx.annotation.NonNull;
     import androidx.annotation.RequiresPermission;
    -import androidx.core.content.IntentCompat;
     
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
    @@ -104,10 +102,15 @@
             return out;
         }
     
    +    @Override
    +    public boolean select(@NonNull SystemRouteItem item) {
    +        throw new UnsupportedOperationException();
    +    }
    +
         @NonNull
         @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    -    private static SystemRouteItem createRouteItemFor(@NonNull BluetoothDevice device) {
    -        return new SystemRouteItem.Builder(/* id= */ device.getAddress())
    +    private SystemRouteItem createRouteItemFor(@NonNull BluetoothDevice device) {
    +        return new SystemRouteItem.Builder(getSourceId(), /* id= */ device.getAddress())
                     .setName(device.getName())
                     .setAddress(device.getAddress())
                     .build();
    @@ -117,27 +120,14 @@
             @Override
             @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
             public void onReceive(Context context, Intent intent) {
    -            BluetoothDevice device = IntentCompat.getParcelableExtra(intent,
    -                    BluetoothDevice.EXTRA_DEVICE, android.bluetooth.BluetoothDevice.class);
    -
                 switch (intent.getAction()) {
                     case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                     case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                     case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
    -                    handleConnectionStateChanged(intent, device);
    +                    mOnRoutesChangedListener.run();
                         break;
    -            }
    -        }
    -
    -        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    -        private void handleConnectionStateChanged(Intent intent,
    -                BluetoothDevice device) {
    -            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
    -            if (state == BluetoothProfile.STATE_CONNECTED) {
    -                mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(device));
    -            } else if (state == BluetoothProfile.STATE_DISCONNECTING
    -                    || state == BluetoothProfile.STATE_DISCONNECTED) {
    -                mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(device));
    +                default:
    +                    // Do nothing.
                 }
             }
         }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
    index 5785679..1c85f85 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouter2SystemRoutesSource.java
    
    @@ -16,59 +16,61 @@
     
     package com.example.androidx.mediarouting.activities.systemrouting.source;
     
    +import android.annotation.SuppressLint;
     import android.content.Context;
     import android.media.MediaRoute2Info;
     import android.media.MediaRouter2;
     import android.media.RouteDiscoveryPreference;
    +import android.media.RoutingSessionInfo;
     import android.os.Build;
     
    +import androidx.annotation.DoNotInline;
     import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
     import androidx.annotation.RequiresApi;
     
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
     
    +import java.lang.reflect.InvocationTargetException;
    +import java.lang.reflect.Method;
     import java.util.ArrayList;
    -import java.util.Collections;
    -import java.util.HashMap;
    +import java.util.Arrays;
     import java.util.List;
    -import java.util.Map;
    +import java.util.Optional;
    +import java.util.Set;
    +import java.util.concurrent.Executor;
    +import java.util.stream.Collectors;
     
     /** Implements {@link SystemRoutesSource} using {@link MediaRouter2}. */
     @RequiresApi(Build.VERSION_CODES.R)
     public final class MediaRouter2SystemRoutesSource extends SystemRoutesSource {
     
    -    @NonNull
    -    private Context mContext;
    -    @NonNull
    -    private MediaRouter2 mMediaRouter2;
    +    @NonNull private final Context mContext;
    +    @NonNull private final MediaRouter2 mMediaRouter2;
    +    @Nullable private final Method mSuitabilityStatusMethod;
    +    @Nullable private final Method mWasTransferInitiatedBySelfMethod;
    +    @Nullable private final Method mTransferReasonMethod;
    +    @NonNull private final ArrayList mRouteItems = new ArrayList<>();
     
         @NonNull
    -    private final Map mLastKnownRoutes = new HashMap<>();
    -    @NonNull
    -    private final MediaRouter2.RouteCallback mRouteCallback = new MediaRouter2.RouteCallback() {
    -        @Override
    -        public void onRoutesUpdated(@NonNull List routes) {
    -            super.onRoutesUpdated(routes);
    -
    -            Map routesLookup = new HashMap<>();
    -            for (MediaRoute2Info route: routes) {
    -                if (!mLastKnownRoutes.containsKey(route.getId())) {
    -                    mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(route));
    +    private final MediaRouter2.RouteCallback mRouteCallback =
    +            new MediaRouter2.RouteCallback() {
    +                @Override
    +                public void onRoutesUpdated(@NonNull List routes) {
    +                    populateRouteItems(routes);
                     }
    -                routesLookup.put(route.getId(), route);
    -            }
    +            };
     
    -            for (MediaRoute2Info route: mLastKnownRoutes.values()) {
    -                if (!routesLookup.containsKey(route.getId())) {
    -                    mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(route));
    +    @NonNull
    +    private final MediaRouter2.ControllerCallback mControllerCallback =
    +            new MediaRouter2.ControllerCallback() {
    +                @Override
    +                public void onControllerUpdated(
    +                        @NonNull MediaRouter2.RoutingController unusedController) {
    +                    populateRouteItems(mMediaRouter2.getRoutes());
                     }
    -            }
    -
    -            mLastKnownRoutes.clear();
    -            mLastKnownRoutes.putAll(routesLookup);
    -        }
    -    };
    +            };
     
         /** Returns a new instance. */
         @NonNull
    @@ -81,22 +83,45 @@
                 @NonNull MediaRouter2 mediaRouter2) {
             mContext = context;
             mMediaRouter2 = mediaRouter2;
    +
    +        Method suitabilityStatusMethod = null;
    +        Method wasTransferInitiatedBySelfMethod = null;
    +        Method transferReasonMethod = null;
    +        // TODO: b/336510942 - Remove reflection once these APIs are available in
    +        // androidx-platform-dev.
    +        try {
    +            suitabilityStatusMethod =
    +                    MediaRoute2Info.class.getDeclaredMethod("getSuitabilityStatus");
    +            wasTransferInitiatedBySelfMethod =
    +                    MediaRouter2.RoutingController.class.getDeclaredMethod(
    +                            "wasTransferInitiatedBySelf");
    +            transferReasonMethod = RoutingSessionInfo.class.getDeclaredMethod("getTransferReason");
    +        } catch (NoSuchMethodException | IllegalAccessError e) {
    +        }
    +        mSuitabilityStatusMethod = suitabilityStatusMethod;
    +        mWasTransferInitiatedBySelfMethod = wasTransferInitiatedBySelfMethod;
    +        mTransferReasonMethod = transferReasonMethod;
         }
     
         @Override
         public void start() {
             RouteDiscoveryPreference routeDiscoveryPreference =
                     new RouteDiscoveryPreference.Builder(
    -                        /* preferredFeatures= */ Collections.emptyList(),
    -                        /* activeScan= */ false)
    +                                /* preferredFeatures= */ Arrays.asList(
    +                                        MediaRoute2Info.FEATURE_LIVE_AUDIO,
    +                                        MediaRoute2Info.FEATURE_LIVE_VIDEO),
    +                                /* activeScan= */ false)
                             .build();
     
    -        mMediaRouter2.registerRouteCallback(mContext.getMainExecutor(),
    -                mRouteCallback, routeDiscoveryPreference);
    +        Executor mainExecutor = mContext.getMainExecutor();
    +        mMediaRouter2.registerRouteCallback(mainExecutor, mRouteCallback, routeDiscoveryPreference);
    +        mMediaRouter2.registerControllerCallback(mainExecutor, mControllerCallback);
    +        populateRouteItems(mMediaRouter2.getRoutes());
         }
     
         @Override
         public void stop() {
    +        mMediaRouter2.unregisterControllerCallback(mControllerCallback);
             mMediaRouter2.unregisterRouteCallback(mRouteCallback);
         }
     
    @@ -109,28 +134,141 @@
         @NonNull
         @Override
         public List fetchSourceRouteItems() {
    -        List out = new ArrayList<>();
    +        return mRouteItems;
    +    }
     
    -        for (MediaRoute2Info routeInfo : mMediaRouter2.getRoutes()) {
    -            if (!routeInfo.isSystemRoute()) {
    -                continue;
    -            }
    -
    -            if (!mLastKnownRoutes.containsKey(routeInfo.getId())) {
    -                mLastKnownRoutes.put(routeInfo.getId(), routeInfo);
    -            }
    -
    -            out.add(createRouteItemFor(routeInfo));
    +    @Override
    +    public boolean select(@NonNull SystemRouteItem item) {
    +        Optional route =
    +                mMediaRouter2.getRoutes().stream()
    +                        .filter(it -> it.getId().equals(item.mId))
    +                        .findFirst();
    +        if (!route.isPresent()) {
    +            return false;
    +        } else {
    +            mMediaRouter2.transferTo(route.get());
    +            return true;
             }
    +    }
     
    -        return out;
    +    // BanUncheckedReflection: See b/336510942 for details on why reflection is needed.
    +    // NewApi: We don't need to check the API level because the transfer reason method is only
    +    // available on API 35, which is greater than API 34, where getRoutingSessionInfo was added.
    +    @SuppressLint({"BanUncheckedReflection", "NewApi"})
    +    private void populateRouteItems(List routes) {
    +        MediaRouter2.RoutingController systemController = mMediaRouter2.getSystemController();
    +        Set selectedRoutesIds =
    +                systemController.getSelectedRoutes().stream()
    +                        .map(MediaRoute2Info::getId)
    +                        .collect(Collectors.toSet());
    +        Boolean selectionInitiatedBySelf = null;
    +        Integer sessionTransferReason = null;
    +        try {
    +            if (mSuitabilityStatusMethod != null) {
    +                selectionInitiatedBySelf =
    +                        (Boolean) mWasTransferInitiatedBySelfMethod.invoke(systemController);
    +            }
    +            if (mTransferReasonMethod != null) {
    +                sessionTransferReason =
    +                        (Integer)
    +                                mTransferReasonMethod.invoke(
    +                                        Api34Impl.getRoutingSessionInfo(systemController));
    +            }
    +        } catch (IllegalAccessException | InvocationTargetException e) {
    +        }
    +        // We need to filter out non-system routes, which might be reported as a result of other
    +        // callbacks with non-system route features being registered in the router.
    +        List systemRoutes =
    +                routes.stream().filter(MediaRoute2Info::isSystemRoute).collect(Collectors.toList());
    +
    +        mRouteItems.clear();
    +        for (MediaRoute2Info route : systemRoutes) {
    +            boolean isSelectedRoute = selectedRoutesIds.contains(route.getId());
    +            Boolean wasTransferredBySelf = isSelectedRoute ? selectionInitiatedBySelf : null;
    +            Integer routeTransferReason = isSelectedRoute ? sessionTransferReason : null;
    +            mRouteItems.add(
    +                    createRouteItemFor(
    +                            route, isSelectedRoute, wasTransferredBySelf, routeTransferReason));
    +        }
    +        mOnRoutesChangedListener.run();
         }
     
         @NonNull
    -    private static SystemRouteItem createRouteItemFor(@NonNull MediaRoute2Info routeInfo) {
    -        return new SystemRouteItem.Builder(routeInfo.getId())
    -                .setName(String.valueOf(routeInfo.getName()))
    -                .setDescription(String.valueOf(routeInfo.getDescription()))
    -                .build();
    +    private SystemRouteItem createRouteItemFor(
    +            @NonNull MediaRoute2Info routeInfo,
    +            boolean isSelectedRoute,
    +            @Nullable Boolean wasTransferredBySelf,
    +            @Nullable Integer transferReason) {
    +        SystemRouteItem.Builder builder =
    +                new SystemRouteItem.Builder(getSourceId(), routeInfo.getId())
    +                        .setName(String.valueOf(routeInfo.getName()))
    +                        .setSelectionSupportState(
    +                                isSelectedRoute
    +                                        ? SystemRouteItem.SelectionSupportState.RESELECTABLE
    +                                        : SystemRouteItem.SelectionSupportState.SELECTABLE)
    +                        .setDescription(String.valueOf(routeInfo.getDescription()))
    +                        .setTransferInitiatedBySelf(wasTransferredBySelf)
    +                        .setTransferReason(getHumanReadableTransferReason(transferReason));
    +        try {
    +            if (mSuitabilityStatusMethod != null) {
    +                // See b/336510942 for details on why reflection is needed.
    +                @SuppressLint("BanUncheckedReflection")
    +                int status = (Integer) mSuitabilityStatusMethod.invoke(routeInfo);
    +                builder.setSuitabilityStatus(getHumanReadableSuitabilityStatus(status));
    +                // TODO: b/319645714 - Populate wasTransferInitiatedBySelf. For that we need to
    +                // change the implementation of this class to use the routing controller instead
    +                // of a route callback.
    +            }
    +        } catch (IllegalAccessException | InvocationTargetException e) {
    +        }
    +        return builder.build();
    +    }
    +
    +    @NonNull
    +    private String getHumanReadableSuitabilityStatus(@Nullable Integer status) {
    +        if (status == null) {
    +            // The route is not selected, or this Android version doesn't support suitability
    +            // status.
    +            return null;
    +        }
    +        switch (status) {
    +            case 0:
    +                return "SUITABLE_FOR_DEFAULT_TRANSFER";
    +            case 1:
    +                return "SUITABLE_FOR_MANUAL_TRANSFER";
    +            case 2:
    +                return "NOT_SUITABLE_FOR_TRANSFER";
    +            default:
    +                return "UNKNOWN(" + status + ")";
    +        }
    +    }
    +
    +    @NonNull
    +    private String getHumanReadableTransferReason(@Nullable Integer transferReason) {
    +        if (transferReason == null) {
    +            // The route is not selected, or this Android version doesn't support transfer reason.
    +            return null;
    +        }
    +        switch (transferReason) {
    +            case 0:
    +                return "FALLBACK";
    +            case 1:
    +                return "SYSTEM_REQUEST";
    +            case 2:
    +                return "APP";
    +            default:
    +                return "UNKNOWN(" + transferReason + ")";
    +        }
    +    }
    +
    +    @RequiresApi(34)
    +    private static final class Api34Impl {
    +        private Api34Impl() {}
    +
    +        @DoNotInline
    +        static RoutingSessionInfo getRoutingSessionInfo(
    +                MediaRouter2.RoutingController routingController) {
    +            return routingController.getRoutingSessionInfo();
    +        }
         }
     }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
    index 438ccfc..cad6fa1 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/MediaRouterSystemRoutesSource.java
    
    @@ -34,19 +34,19 @@
         private final MediaRouter mMediaRouter;
     
         @NonNull
    -    private final MediaRouter.Callback mCallback = new MediaRouter.SimpleCallback() {
    -        @Override
    -        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
    -            super.onRouteAdded(router, info);
    -            mOnRoutesChangedListener.onRouteAdded(createRouteItemFor(info));
    -        }
    +    private final MediaRouter.Callback mCallback =
    +            new MediaRouter.SimpleCallback() {
    +                @Override
    +                public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
    +                    mOnRoutesChangedListener.run();
    +                }
     
    -        @Override
    -        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
    -            super.onRouteRemoved(router, info);
    -            mOnRoutesChangedListener.onRouteRemoved(createRouteItemFor(info));
    -        }
    -    };
    +                @Override
    +                public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
    +                    super.onRouteRemoved(router, info);
    +                    mOnRoutesChangedListener.run();
    +                }
    +            };
     
         /** Returns a new instance. */
         @NonNull
    @@ -94,10 +94,15 @@
             return out;
         }
     
    +    @Override
    +    public boolean select(@NonNull SystemRouteItem item) {
    +        throw new UnsupportedOperationException();
    +    }
    +
         @NonNull
    -    private static SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
    +    private SystemRouteItem createRouteItemFor(@NonNull MediaRouter.RouteInfo routeInfo) {
             SystemRouteItem.Builder builder =
    -                new SystemRouteItem.Builder(/* id= */ routeInfo.getName().toString())
    +                new SystemRouteItem.Builder(getSourceId(), /* id= */ routeInfo.getName().toString())
                             .setName(routeInfo.getName().toString());
     
             CharSequence description = routeInfo.getDescription();
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
    index f5a32db..bad6317 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/systemrouting/source/SystemRoutesSource.java
    
    @@ -17,7 +17,6 @@
     package com.example.androidx.mediarouting.activities.systemrouting.source;
     
     import androidx.annotation.NonNull;
    -import androidx.annotation.Nullable;
     
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRouteItem;
     import com.example.androidx.mediarouting.activities.systemrouting.SystemRoutesSourceItem;
    @@ -29,23 +28,11 @@
      */
     public abstract class SystemRoutesSource {
     
    -    private static final NoOpOnRoutesChangedListener NO_OP_ON_ROUTES_CHANGED_LISTENER =
    -            new NoOpOnRoutesChangedListener();
    +    @NonNull protected Runnable mOnRoutesChangedListener = () -> {};
     
    -    @NonNull
    -    protected OnRoutesChangedListener mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;
    -
    -    /**
    -     * Sets {@link OnRoutesChangedListener} and subscribes to the source updates.
    -     * To unsubscribe from the routes update pass {@code null} instead of the listener.
    -     */
    -    public void setOnRoutesChangedListener(
    -            @Nullable OnRoutesChangedListener onRoutesChangedListener) {
    -        if (onRoutesChangedListener != null) {
    -            mOnRoutesChangedListener = onRoutesChangedListener;
    -        } else {
    -            mOnRoutesChangedListener = NO_OP_ON_ROUTES_CHANGED_LISTENER;
    -        }
    +    /** Sets a {@link Runnable} to invoke whenever routes change. */
    +    public void setOnRoutesChangedListener(@NonNull Runnable onRoutesChangedListener) {
    +        mOnRoutesChangedListener = onRoutesChangedListener;
         }
     
         /**
    @@ -63,6 +50,12 @@
             // Empty on purpose.
         }
     
    +    /** Returns a string that uniquely identifies this source. */
    +    @NonNull
    +    public final String getSourceId() {
    +        return getClass().getSimpleName();
    +    }
    +
         /**
          * Gets a source item containing source type.
          */
    @@ -76,39 +69,11 @@
         public abstract List fetchSourceRouteItems();
     
         /**
    -     * An interface for listening to routes changes: whether the route has been added or removed
    -     * from the source.
    +     * Selects the route that corresponds to the given item.
    +     *
    +     * @param item An item with {@link SystemRouteItem#mSelectionSupportState} {@link
    +     *     SystemRouteItem.SelectionSupportState#SELECTABLE}.
    +     * @return Whether the selection was successful.
          */
    -    public interface OnRoutesChangedListener {
    -
    -        /**
    -         * Called when a route has been added to the source's routes list.
    -         *
    -         * @param routeItem a newly added route.
    -         */
    -        void onRouteAdded(@NonNull SystemRouteItem routeItem);
    -
    -        /**
    -         * Called when a route has been removed from the source's routes list.
    -         *
    -         * @param routeItem a recently removed route.
    -         */
    -        void onRouteRemoved(@NonNull SystemRouteItem routeItem);
    -    }
    -
    -    /**
    -     * Default no-op implementation of {@link OnRoutesChangedListener}.
    -     * Used as a fallback implement when there is no listener.
    -     */
    -    private static final class NoOpOnRoutesChangedListener implements OnRoutesChangedListener {
    -        @Override
    -        public void onRouteAdded(@NonNull SystemRouteItem routeItem) {
    -            // Empty on purpose.
    -        }
    -
    -        @Override
    -        public void onRouteRemoved(@NonNull SystemRouteItem routeItem) {
    -            // Empty on purpose.
    -        }
    -    }
    +    public abstract boolean select(@NonNull SystemRouteItem item);
     }
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
    index 47ffed2..a597796 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/SampleDynamicGroupMediaRouteProvider.java
    
    @@ -304,7 +304,16 @@
                 for (String memberRouteId : mMemberRouteIds) {
                     groupDescriptorBuilder.addGroupMemberId(memberRouteId);
                 }
    -
    +            if (!mMemberRouteIds.isEmpty()) {
    +                DynamicRouteDescriptor firstDynamicRouteDescriptor =
    +                        mDynamicRouteDescriptors.get(mMemberRouteIds.get(0));
    +                if (firstDynamicRouteDescriptor != null) {
    +                    String name = firstDynamicRouteDescriptor.getRouteDescriptor().getName();
    +                    int sizeMinusOne = mMemberRouteIds.size() - 1;
    +                    String nameSuffix = sizeMinusOne == 0 ? "" : (" + " + sizeMinusOne);
    +                    groupDescriptorBuilder.setName(name + nameSuffix);
    +                }
    +            }
                 mGroupDescriptor = groupDescriptorBuilder.build();
             }
     
    
    diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
    index 658c256..abbfbf2 100644
    --- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
    +++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/services/SampleDynamicGroupMediaRouteProviderService.java
    
    @@ -58,12 +58,16 @@
     
         /** Reload all routes provided by this service. */
         public void reloadRoutes() {
    -        mDynamicGroupMediaRouteProvider.reloadRoutes();
    +        if (mDynamicGroupMediaRouteProvider != null) {
    +            mDynamicGroupMediaRouteProvider.reloadRoutes();
    +        }
         }
     
         /** Reload the flag for isDynamicRouteEnabled. */
         public void reloadDynamicRoutesEnabled() {
    -        mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();
    +        if (mDynamicGroupMediaRouteProvider != null) {
    +            mDynamicGroupMediaRouteProvider.reloadDynamicRoutesEnabled();
    +        }
         }
     
         /**
    
    diff --git a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
    index 2572fc6..50418cb 100644
    --- a/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
    +++ b/samples/MediaRoutingDemo/src/main/res/layout/activity_settings.xml
    
    @@ -91,6 +91,56 @@
                 android:layout_margin="12dp"
                 android:padding="4dp">
     
    +            
    +                android:id="@+id/enable_simple_provider_switch"
    +                android:layout_width="wrap_content"
    +                android:layout_height="match_parent"
    +                android:layout_alignParentEnd="true"
    +                android:layout_alignParentRight="true"
    +                android:layout_centerVertical="true" />
    +
    +            
    +                android:layout_width="wrap_content"
    +                android:layout_height="wrap_content"
    +                android:layout_alignParentLeft="true"
    +                android:layout_alignParentStart="true"
    +                android:layout_centerVertical="true"
    +                android:gravity="center"
    +                android:text="Enable simple route provider" />
    +
    +        
    +
    +        
    +            android:layout_width="match_parent"
    +            android:layout_height="50dp"
    +            android:layout_margin="12dp"
    +            android:padding="4dp">
    +
    +            
    +                android:id="@+id/enable_dynamic_provider_switch"
    +                android:layout_width="wrap_content"
    +                android:layout_height="match_parent"
    +                android:layout_alignParentEnd="true"
    +                android:layout_alignParentRight="true"
    +                android:layout_centerVertical="true" />
    +
    +            
    +                android:layout_width="wrap_content"
    +                android:layout_height="wrap_content"
    +                android:layout_alignParentLeft="true"
    +                android:layout_alignParentStart="true"
    +                android:layout_centerVertical="true"
    +                android:gravity="center"
    +                android:text="Enable dynamic route provider" />
    +
    +        
    +
    +        
    +            android:layout_width="match_parent"
    +            android:layout_height="50dp"
    +            android:layout_margin="12dp"
    +            android:padding="4dp">
    +
                 
                     android:id="@+id/dialog_spinner"
                     android:layout_width="wrap_content"
    
    diff --git a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
    index 5d54748..8b3ac4e 100644
    --- a/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
    +++ b/samples/MediaRoutingDemo/src/main/res/layout/item_system_route.xml
    
    @@ -77,6 +77,49 @@
                     android:textSize="14sp"
                     tools:text="This is a description of an amazing system route." />
     
    +            
    +                android:id="@+id/route_suitability_status"
    +                android:layout_marginTop="8dp"
    +                android:layout_width="wrap_content"
    +                android:layout_height="wrap_content"
    +                android:layout_below="@+id/route_description"
    +                android:layout_alignParentLeft="true"
    +                android:layout_alignParentStart="true"
    +                android:textSize="14sp"
    +                tools:text="Suitability status." />
    +
    +            
    +                android:id="@+id/route_transfer_initiated_by_self"
    +                android:layout_marginTop="8dp"
    +                android:layout_width="wrap_content"
    +                android:layout_height="wrap_content"
    +                android:layout_below="@+id/route_suitability_status"
    +                android:layout_alignParentLeft="true"
    +                android:layout_alignParentStart="true"
    +                android:textSize="14sp"
    +                tools:text="Transfer initiated by self." />
    +
    +            
    +                android:id="@+id/route_transfer_reason"
    +                android:layout_marginTop="8dp"
    +                android:layout_width="wrap_content"
    +                android:layout_height="wrap_content"
    +                android:layout_below="@+id/route_transfer_initiated_by_self"
    +                android:layout_alignParentLeft="true"
    +                android:layout_alignParentStart="true"
    +                android:textSize="14sp"
    +                tools:text="Transfer reason." />
    +
    +            
    +                android:id="@+id/route_selection_button"
    +                android:layout_width="wrap_content"
    +                android:layout_height="wrap_content"
    +                android:layout_below="@+id/route_transfer_reason"
    +                android:layout_alignParentLeft="true"
    +                android:layout_alignParentStart="true"
    +                android:textSize="14sp"
    +                tools:text="Selection button" />
    +
             
     
         
    
    diff --git a/settings.gradle b/settings.gradle
    index d8806ea..7720ca8 100644
    --- a/settings.gradle
    +++ b/settings.gradle
    
    @@ -96,7 +96,8 @@
             value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
             value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
     
    -        publishing.onlyIf { it.authenticated }
    +        // Do not publish scan for androidx-platform-dev
    +        publishing.onlyIf { false }
         }
     }
     
    @@ -1088,6 +1089,8 @@
     File repoRoot = new File(rootDir, "../..").canonicalFile
     
     includeOptionalProject(":xr:xr", new File(repoRoot, "xr/xr"), [BuildType.XR])
    +includeOptionalProject(":xr:integration-tests:compose-adaptive-sample", new File(repoRoot, "xr/integration-tests/compose-adaptive-sample"), [BuildType.XR])
    +includeOptionalProject(":xr:xr-material3-adaptive", new File(repoRoot, "xr/xr-material3-adaptive"), [BuildType.XR])
     
     /////////////////////////////
     //
    
    diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
    index 969c473..f626fd9 100644
    --- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
    +++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/RemoteActivityHelper.kt
    
    @@ -43,9 +43,7 @@
     /**
      * Support for opening android intents on other devices.
      *
    - *
      * The following example opens play store for the given app on another device:
    - *
      * ```
      * val remoteActivityHelper = RemoteActivityHelper(context, executor)
      *
    @@ -57,23 +55,24 @@
      *     nodeId)
      * ```
      *
    - * [startRemoteActivity] returns a [ListenableFuture], which is completed after the intent has
    - * been sent or failed if there was an issue with sending the intent.
    + * [startRemoteActivity] returns a [ListenableFuture], which is completed after the intent has been
    + * sent or failed if there was an issue with sending the intent.
      *
      * nodeId is the opaque string that represents a
    - * [node](https://developers.google.com/android/reference/com/google/android/gms/wearable/Node)
    - * in the Android Wear network. For the given device, it can obtained by `NodeClient.getLocalNode()`
    + * [node](https://developers.google.com/android/reference/com/google/android/gms/wearable/Node) in
    + * the Android Wear network. For the given device, it can obtained by `NodeClient.getLocalNode()`
      * and the list of nodes to which this device is currently connected can be obtained by
      * `NodeClient.getConnectedNodes()`. More information about this can be found
      * [here](https://developers.google.com/android/reference/com/google/android/gms/wearable/NodeClient).
      *
      * @param context The [Context] of the application for sending the intent.
    - * @param executor [Executor] used for getting data to be passed in remote intent. If not
    - * specified, default will be `Executors.newSingleThreadExecutor()`.
    + * @param executor [Executor] used for getting data to be passed in remote intent. If not specified,
    + *   default will be `Executors.newSingleThreadExecutor()`.
      */
     /* ktlint-enable max-line-length */
     public class RemoteActivityHelper
    -    @JvmOverloads constructor(
    +@JvmOverloads
    +constructor(
         private val context: Context,
         private val executor: Executor = Executors.newSingleThreadExecutor()
     ) {
    @@ -121,7 +120,7 @@
              */
             public const val RESULT_OK: Int = 0
     
    -        /** Result code passed to [ResultReceiver.send] when a remote intent failed to send.  */
    +        /** Result code passed to [ResultReceiver.send] when a remote intent failed to send. */
             public const val RESULT_FAILED: Int = 1
     
             internal const val DEFAULT_PACKAGE = "com.google.android.wearable.app"
    @@ -144,8 +143,7 @@
              * @return The node id, or null if none was set.
              */
             @JvmStatic
    -        public fun getTargetNodeId(intent: Intent): String? =
    -            intent.getStringExtra(EXTRA_NODE_ID)
    +        public fun getTargetNodeId(intent: Intent): String? = intent.getStringExtra(EXTRA_NODE_ID)
     
             /**
              * Returns the [android.os.ResultReceiver] extra of remote intent.
    @@ -158,7 +156,7 @@
             internal fun getRemoteIntentResultReceiver(intent: Intent): ResultReceiver? =
                 intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
     
    -        /** Re-package a result receiver as a vanilla version for cross-process sending  */
    +        /** Re-package a result receiver as a vanilla version for cross-process sending */
             @JvmStatic
             internal fun getResultReceiverForSending(receiver: ResultReceiver): ResultReceiver {
                 val parcel = Parcel.obtain()
    @@ -170,11 +168,8 @@
             }
         }
     
    -    /**
    -     * Used for testing only, so we can set mock NodeClient.
    -     */
    -    @VisibleForTesting
    -    internal var nodeClient: NodeClient = Wearable.getNodeClient(context)
    +    /** Used for testing only, so we can set mock NodeClient. */
    +    @VisibleForTesting internal var nodeClient: NodeClient = Wearable.getNodeClient(context)
     
         /** Used for testing only, so we can mock wear sdk dependency. */
         @VisibleForTesting internal var remoteInteractionsManager: IRemoteInteractionsManager = RemoteInteractionsManagerCompat(context)
    @@ -231,24 +226,23 @@
         }
     
         /**
    -     * Start an activity on another device. This api currently supports sending intents with
    -     * action set to [android.content.Intent.ACTION_VIEW], a data uri populated using
    +     * Start an activity on another device. This api currently supports sending intents with action
    +     * set to [android.content.Intent.ACTION_VIEW], a data uri populated using
          * [android.content.Intent.setData], and with the category
    -     * [android.content.Intent.CATEGORY_BROWSABLE] present. If the current device is a watch,
    -     * the activity will start on the companion phone device. Otherwise, the activity will
    -     * start on all connected watch devices.
    +     * [android.content.Intent.CATEGORY_BROWSABLE] present. If the current device is a watch, the
    +     * activity will start on the companion phone device. Otherwise, the activity will start on all
    +     * connected watch devices.
          *
    -     * @param targetIntent   The intent to open on the remote device. Action must be set to
    -     *                       [android.content.Intent.ACTION_VIEW], a data uri must be populated
    -     *                       using [android.content.Intent.setData], and the category
    -     *                       [android.content.Intent.CATEGORY_BROWSABLE] must be present.
    -     * @param targetNodeId   Wear OS node id for the device where the activity should be
    -     *                       started. If null, and the current device is a watch, the
    -     *                       activity will start on the companion phone device. Otherwise,
    -     *                       the activity will start on all connected watch devices.
    -     * @return The [ListenableFuture] which resolves if starting activity was successful or
    -     * throws [Exception] if any errors happens. If there's a problem with starting remote
    -     * activity, [RemoteIntentException] will be thrown.
    +     * @param targetIntent The intent to open on the remote device. Action must be set to
    +     *   [android.content.Intent.ACTION_VIEW], a data uri must be populated using
    +     *   [android.content.Intent.setData], and the category
    +     *   [android.content.Intent.CATEGORY_BROWSABLE] must be present.
    +     * @param targetNodeId Wear OS node id for the device where the activity should be started. If
    +     *   null, and the current device is a watch, the activity will start on the companion phone
    +     *   device. Otherwise, the activity will start on all connected watch devices.
    +     * @return The [ListenableFuture] which resolves if starting activity was successful or throws
    +     *   [Exception] if any errors happens. If there's a problem with starting remote activity,
    +     *   [RemoteIntentException] will be thrown.
          */
         @JvmOverloads
         public fun startRemoteActivity(
    @@ -268,7 +262,10 @@
                 }
     
                 startCreatingIntentForRemoteActivity(
    -                targetIntent, targetNodeId, it, nodeClient,
    +                targetIntent,
    +                targetNodeId,
    +                it,
    +                nodeClient,
                     object : Callback {
                         override fun intentCreated(intent: Intent) {
                             context.sendBroadcast(intent)
    @@ -302,10 +299,9 @@
             }
     
             if (nodeId != null) {
    -            nodeClient.getCompanionPackageForNode(nodeId)
    -                .addOnSuccessListener(
    -                    executor
    -                ) { taskPackageName ->
    +            nodeClient
    +                .getCompanionPackageForNode(nodeId)
    +                .addOnSuccessListener(executor) { taskPackageName ->
                         val packageName = taskPackageName ?: DEFAULT_PACKAGE
     
                         if (packageName.isEmpty()) {
    @@ -320,35 +316,37 @@
                                 )
                             )
                         }
    -                }.addOnFailureListener(executor) { callback.onFailure(it) }
    +                }
    +                .addOnFailureListener(executor) { callback.onFailure(it) }
                 return
             }
     
    -        nodeClient.connectedNodes.addOnSuccessListener(
    -            executor
    -        ) { connectedNodes ->
    -            if (connectedNodes.size == 0) {
    -                callback.onFailure(NotFoundException("No devices connected"))
    -            } else {
    -                val resultReceiver = RemoteIntentResultReceiver(completer, connectedNodes.size)
    -                for (node in connectedNodes) {
    -                    nodeClient.getCompanionPackageForNode(node.id).addOnSuccessListener(
    -                        executor
    -                    ) { taskPackageName ->
    -                        val packageName = taskPackageName ?: DEFAULT_PACKAGE
    -                        callback.intentCreated(
    -                            createIntent(intent, resultReceiver, node.id, packageName)
    -                        )
    -                    }.addOnFailureListener(executor) { callback.onFailure(it) }
    +        nodeClient.connectedNodes
    +            .addOnSuccessListener(executor) { connectedNodes ->
    +                if (connectedNodes.size == 0) {
    +                    callback.onFailure(NotFoundException("No devices connected"))
    +                } else {
    +                    val resultReceiver = RemoteIntentResultReceiver(completer, connectedNodes.size)
    +                    for (node in connectedNodes) {
    +                        nodeClient
    +                            .getCompanionPackageForNode(node.id)
    +                            .addOnSuccessListener(executor) { taskPackageName ->
    +                                val packageName = taskPackageName ?: DEFAULT_PACKAGE
    +                                callback.intentCreated(
    +                                    createIntent(intent, resultReceiver, node.id, packageName)
    +                                )
    +                            }
    +                            .addOnFailureListener(executor) { callback.onFailure(it) }
    +                    }
                     }
                 }
    -        }.addOnFailureListener(executor) { callback.onFailure(it) }
    +            .addOnFailureListener(executor) { callback.onFailure(it) }
         }
     
         /**
    -     * Creates [android.content.Intent] with action specifying remote intent. If any of
    -     * additional extras are specified, they will be added to it. If specified, [ResultReceiver]
    -     * will be re-packed to be parcelable. If specified, packageName will be set.
    +     * Creates [android.content.Intent] with action specifying remote intent. If any of additional
    +     * extras are specified, they will be added to it. If specified, [ResultReceiver] will be
    +     * re-packed to be parcelable. If specified, packageName will be set.
          */
         @VisibleForTesting
         internal fun createIntent(
    @@ -371,9 +369,7 @@
             return remoteIntent
         }
     
    -    /**
    -     * Result code passed to [ResultReceiver.send] for the status of remote intent.
    -     */
    +    /** Result code passed to [ResultReceiver.send] for the status of remote intent. */
         @IntDef(RESULT_OK, RESULT_FAILED)
         @Retention(AnnotationRetention.SOURCE)
         internal annotation class SendResult
    @@ -382,6 +378,7 @@
     
         private interface Callback {
             fun intentCreated(intent: Intent)
    +
             fun onFailure(exception: Exception)
         }
     
    
    diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
    index d36cd18..85a2586 100644
    --- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
    +++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
    
    @@ -26,19 +26,17 @@
      * * ones creating Intents
      * * ones receiving and responding to those Intents.
      *
    - *
    - * To register a configuration activity for a watch face, add a `` entry to the
    - * watch face component in its Android Manifest file with an intent action to be fired to start the
    + * To register a configuration activity for a watch face, add a `` entry to the watch
    + * face component in its Android Manifest file with an intent action to be fired to start the
      * activity. The following meta-data will register the
    - * `androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR` action to be started when
    - * configuring a watch face on the wearable device:
    + * `androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR` action to be started when configuring a
    + * watch face on the wearable device:
      * ```
      * 
      * android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
      * android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
      * ```
      *
    - *
      * To register a configuration activity to be started on a companion phone, add the following
      * alternative meta-data entry to the watch face component:
      * ```
    @@ -47,9 +45,8 @@
      * android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
      * ```
      *
    - *
    - * The activity should have an intent filter which lists the action specified in the meta-data
    - * block above, in addition to the two categories present in the following example:
    + * The activity should have an intent filter which lists the action specified in the meta-data block
    + * above, in addition to the two categories present in the following example:
      * ```
      * 
      * 
    @@ -61,7 +58,6 @@
      * 
      * ```
      *
    - *
      * For phone side configuration activities, substitute the category
      * `com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION` for
      * `com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION`.
    @@ -81,9 +77,9 @@
              * given [Intent]. [ComponentName] is being used to identify the APK and the class of the
              * watch face service.
              *
    -         * @param watchFaceIntent  The intent holding config activity launch.
    -         * @return the value of an item previously added with [putWatchFaceComponentExtra], or
    -         * null if no value was found.
    +         * @param watchFaceIntent The intent holding config activity launch.
    +         * @return the value of an item previously added with [putWatchFaceComponentExtra], or null
    +         *   if no value was found.
              */
             @Suppress("DEPRECATION")
             @JvmStatic
    @@ -111,7 +107,7 @@
              *
              * @param watchFaceIntent The intent holding config activity launch.
              * @return the value of an item previously added with [putPeerIdExtra], or null if no value
    -         *         was found.
    +         *   was found.
              */
             @JvmStatic
             @Nullable
    
    diff --git a/wear/wear-remote-interactions/src/test/java/androidx/wear/remote/interactions/RemoteActivityHelperTest.kt b/wear/wear-remote-interactions/src/test/java/androidx/wear/remote/interactions/RemoteActivityHelperTest.kt
    index 45a06ecee..c6c6ee1 100644
    --- a/wear/wear-remote-interactions/src/test/java/androidx/wear/remote/interactions/RemoteActivityHelperTest.kt
    +++ b/wear/wear-remote-interactions/src/test/java/androidx/wear/remote/interactions/RemoteActivityHelperTest.kt
    
    @@ -86,9 +86,7 @@
             private var altResult = RESULT_OK
     
             override fun onReceive(context: Context?, intent: Intent?) {
    -            val resultReceiver = intent?.let {
    -                getRemoteIntentResultReceiver(it)
    -            }
    +            val resultReceiver = intent?.let { getRemoteIntentResultReceiver(it) }
                 if (result == DIFFERENT_RESULT) {
                     altResult = (altResult + 1) % 2
                     resultReceiver?.send(result, null)
    @@ -104,9 +102,8 @@
         private val testNodeId2 = "Test Node ID2"
         private val testUri = Uri.parse("market://details?id=com.google.android.wearable.app")
         private val context: Context = ApplicationProvider.getApplicationContext()
    -    private val testExtraIntent = Intent(Intent.ACTION_VIEW)
    -        .addCategory(Intent.CATEGORY_BROWSABLE)
    -        .setData(testUri)
    +    private val testExtraIntent =
    +        Intent(Intent.ACTION_VIEW).addCategory(Intent.CATEGORY_BROWSABLE).setData(testUri)
         private lateinit var mRemoteActivityHelper: RemoteActivityHelper
     
         @Mock private var mockNodeClient: NodeClient = mock()
    @@ -124,7 +121,8 @@
         private fun setSystemFeatureWatch(isWatch: Boolean) {
             val shadowPackageManager = shadowOf(context.packageManager)
             shadowPackageManager!!.setSystemFeature(
    -            RemoteInteractionsUtil.SYSTEM_FEATURE_WATCH, isWatch
    +            RemoteInteractionsUtil.SYSTEM_FEATURE_WATCH,
    +            isWatch
             )
         }
     
    @@ -142,28 +140,24 @@
     
         @Test
         fun testStartRemoteActivity_notActionViewIntent() {
    -        assertThrows(
    -            ExecutionException::class.java
    -        ) { mRemoteActivityHelper.startRemoteActivity(Intent(), testNodeId).get() }
    +        assertThrows(ExecutionException::class.java) {
    +            mRemoteActivityHelper.startRemoteActivity(Intent(), testNodeId).get()
    +        }
         }
     
         @Test
         fun testStartRemoteActivity_dataNull() {
    -        assertThrows(
    -            ExecutionException::class.java
    -        ) {
    +        assertThrows(ExecutionException::class.java) {
                 mRemoteActivityHelper.startRemoteActivity(Intent(Intent.ACTION_VIEW), testNodeId).get()
             }
         }
     
         @Test
         fun testStartRemoteActivity_notCategoryBrowsable() {
    -        assertThrows(
    -            ExecutionException::class.java
    -        ) {
    -            mRemoteActivityHelper.startRemoteActivity(
    -                Intent(Intent.ACTION_VIEW).setData(Uri.EMPTY), testNodeId
    -            ).get()
    +        assertThrows(ExecutionException::class.java) {
    +            mRemoteActivityHelper
    +                .startRemoteActivity(Intent(Intent.ACTION_VIEW).setData(Uri.EMPTY), testNodeId)
    +                .get()
             }
         }
     
    @@ -185,8 +179,7 @@
             }
     
             val broadcastIntents =
    -            shadowOf(ApplicationProvider.getApplicationContext() as Application)
    -                .broadcastIntents
    +            shadowOf(ApplicationProvider.getApplicationContext() as Application).broadcastIntents
             assertEquals(1, broadcastIntents.size)
             val intent = broadcastIntents[0]
             assertEquals(testExtraIntent, getTargetIntent(intent))
    @@ -229,8 +222,7 @@
             }
     
             val broadcastIntents =
    -            shadowOf(ApplicationProvider.getApplicationContext() as Application)
    -                .broadcastIntents
    +            shadowOf(ApplicationProvider.getApplicationContext() as Application).broadcastIntents
             assertEquals(1, broadcastIntents.size)
             assertRemoteIntentEqual(testExtraIntent, testNodeId, testPackageName, broadcastIntents[0])
         }
    @@ -254,8 +246,7 @@
             }
     
             val broadcastIntents =
    -            shadowOf(ApplicationProvider.getApplicationContext() as Application)
    -                .broadcastIntents
    +            shadowOf(ApplicationProvider.getApplicationContext() as Application).broadcastIntents
             assertEquals(1, broadcastIntents.size)
             val intent = broadcastIntents[0]
             assertEquals(testExtraIntent, getTargetIntent(intent))
    @@ -273,9 +264,8 @@
             context.registerReceiver(receiver, IntentFilter(ACTION_REMOTE_INTENT))
     
             try {
    -            val future = mRemoteActivityHelper.startRemoteActivity(
    -                testExtraIntent, targetNodeId = null
    -            )
    +            val future =
    +                mRemoteActivityHelper.startRemoteActivity(testExtraIntent, targetNodeId = null)
                 shadowOf(Looper.getMainLooper()).idle()
                 assertTrue(future.isDone)
                 future.get()
    @@ -287,8 +277,7 @@
     
             shadowOf(Looper.getMainLooper()).idle()
             val broadcastIntents =
    -            shadowOf(ApplicationProvider.getApplicationContext() as Application)
    -                .broadcastIntents
    +            shadowOf(ApplicationProvider.getApplicationContext() as Application).broadcastIntents
             assertEquals(2, broadcastIntents.size)
     
             assertRemoteIntentEqual(testExtraIntent, testNodeId, testPackageName, broadcastIntents[0])
    @@ -305,9 +294,8 @@
             context.registerReceiver(receiver, IntentFilter(ACTION_REMOTE_INTENT))
     
             assertThrows(ExecutionException::class.java) {
    -            val future = mRemoteActivityHelper.startRemoteActivity(
    -                testExtraIntent, targetNodeId = null
    -            )
    +            val future =
    +                mRemoteActivityHelper.startRemoteActivity(testExtraIntent, targetNodeId = null)
                 shadowOf(Looper.getMainLooper()).idle()
                 assertTrue(future.isDone)
                 future.get()
    @@ -316,8 +304,7 @@
     
             shadowOf(Looper.getMainLooper()).idle()
             val broadcastIntents =
    -            shadowOf(ApplicationProvider.getApplicationContext() as Application)
    -                .broadcastIntents
    +            shadowOf(ApplicationProvider.getApplicationContext() as Application).broadcastIntents
             assertEquals(2, broadcastIntents.size)
             assertRemoteIntentEqual(testExtraIntent, testNodeId, testPackageName, broadcastIntents[0])
             assertRemoteIntentEqual(testExtraIntent, testNodeId2, testPackageName2, broadcastIntents[1])
    @@ -333,9 +320,8 @@
             context.registerReceiver(receiver, IntentFilter(ACTION_REMOTE_INTENT))
     
             assertThrows(ExecutionException::class.java) {
    -            val future = mRemoteActivityHelper.startRemoteActivity(
    -                testExtraIntent, targetNodeId = null
    -            )
    +            val future =
    +                mRemoteActivityHelper.startRemoteActivity(testExtraIntent, targetNodeId = null)
                 shadowOf(Looper.getMainLooper()).idle()
                 assertTrue(future.isDone)
                 future.get()
    @@ -344,8 +330,7 @@
     
             shadowOf(Looper.getMainLooper()).idle()
             val broadcastIntents =
    -            shadowOf(ApplicationProvider.getApplicationContext() as Application)
    -                .broadcastIntents
    +            shadowOf(ApplicationProvider.getApplicationContext() as Application).broadcastIntents
             assertEquals(2, broadcastIntents.size)
             assertRemoteIntentEqual(testExtraIntent, testNodeId, testPackageName, broadcastIntents[0])
             assertRemoteIntentEqual(testExtraIntent, testNodeId2, testPackageName2, broadcastIntents[1])
    @@ -384,9 +369,7 @@
             shadowOf(Looper.getMainLooper()).idle()
             assertTrue(future.isDone)
     
    -        val actualException = assertThrows(ExecutionException::class.java) {
    -            future.get()
    -        }
    +        val actualException = assertThrows(ExecutionException::class.java) { future.get() }
     
             assertTrue(actualException.cause is IllegalStateException)
         }
    @@ -404,9 +387,7 @@
             shadowOf(Looper.getMainLooper()).idle()
             assertTrue(future.isDone)
     
    -        val actualException = assertThrows(ExecutionException::class.java) {
    -            future.get()
    -        }
    +        val actualException = assertThrows(ExecutionException::class.java) { future.get() }
     
             assertTrue(actualException.cause is IllegalStateException)
         }
    @@ -425,9 +406,7 @@
             shadowOf(Looper.getMainLooper()).idle()
             assertTrue(future.isDone)
     
    -        val actualException = assertThrows(ExecutionException::class.java) {
    -            future.get()
    -        }
    +        val actualException = assertThrows(ExecutionException::class.java) { future.get() }
     
             assertTrue(actualException.cause is IllegalStateException)
         }
    @@ -442,9 +421,7 @@
             shadowOf(Looper.getMainLooper()).idle()
             assertTrue(future.isDone)
     
    -        val actualException = assertThrows(ExecutionException::class.java) {
    -            future.get()
    -        }
    +        val actualException = assertThrows(ExecutionException::class.java) { future.get() }
     
             assertTrue(actualException.cause is NotFoundException)
         }
    @@ -452,16 +429,13 @@
         @Test
         fun testStartRemoveActivity_noNodes() {
             setSystemFeatureWatch(false)
    -        Mockito.`when`(mockNodeClient.connectedNodes)
    -            .thenReturn(Tasks.forResult(listOf()))
    +        Mockito.`when`(mockNodeClient.connectedNodes).thenReturn(Tasks.forResult(listOf()))
     
             val future = mRemoteActivityHelper.startRemoteActivity(testExtraIntent)
             shadowOf(Looper.getMainLooper()).idle()
             assertTrue(future.isDone)
     
    -        val actualException = assertThrows(ExecutionException::class.java) {
    -            future.get()
    -        }
    +        val actualException = assertThrows(ExecutionException::class.java) { future.get() }
     
             assertTrue(actualException.cause is NotFoundException)
         }
    
    diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
    index 8bf868f..8c8ca6f 100644
    --- a/window/extensions/extensions/api/current.txt
    +++ b/window/extensions/extensions/api/current.txt
    
    @@ -52,17 +52,36 @@
     package androidx.window.extensions.embedding {
     
       public interface ActivityEmbeddingComponent {
    +    method public default void clearActivityStackAttributesCalculator();
    +    method public default void clearEmbeddedActivityWindowInfoCallback();
         method public void clearSplitAttributesCalculator();
         method public void clearSplitInfoCallback();
    -    method public default void finishActivityStacks(java.util.Set);
    +    method @Deprecated public default void finishActivityStacks(java.util.Set);
    +    method public default void finishActivityStacksWithTokens(java.util.Set);
    +    method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
    +    method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
    +    method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
         method public default void invalidateTopVisibleSplitAttributes();
         method public boolean isActivityEmbedded(android.app.Activity);
    +    method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
    +    method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer!>);
    +    method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function);
    +    method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer);
         method public void setEmbeddingRules(java.util.Set);
    -    method public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
    +    method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
         method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function);
         method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer!>);
         method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer!>);
    -    method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
    +    method public default void unpinTopActivityStack(int);
    +    method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer!>);
    +    method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
    +    method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
    +    method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
    +  }
    +
    +  public class ActivityEmbeddingOptionsProperties {
    +    field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
    +    field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
       }
     
       public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
    @@ -81,23 +100,103 @@
     
       public class ActivityStack {
         method public java.util.List getActivities();
    +    method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
    +    method public String? getTag();
         method public boolean isEmpty();
       }
     
    +  public static final class ActivityStack.Token {
    +    method public static androidx.window.extensions.embedding.ActivityStack.Token createFromBinder(android.os.IBinder);
    +    method public static androidx.window.extensions.embedding.ActivityStack.Token readFromBundle(android.os.Bundle);
    +    method public android.os.Bundle toBundle();
    +    field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
    +  }
    +
    +  public final class ActivityStackAttributes {
    +    method public android.graphics.Rect getRelativeBounds();
    +    method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
    +  }
    +
    +  public static final class ActivityStackAttributes.Builder {
    +    ctor public ActivityStackAttributes.Builder();
    +    method public androidx.window.extensions.embedding.ActivityStackAttributes build();
    +    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
    +    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
    +  }
    +
    +  public class ActivityStackAttributesCalculatorParams {
    +    method public String getActivityStackTag();
    +    method public android.os.Bundle getLaunchOptions();
    +    method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
    +  }
    +
    +  public abstract class AnimationBackground {
    +    method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
    +    field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
    +  }
    +
    +  public static class AnimationBackground.ColorBackground extends androidx.window.extensions.embedding.AnimationBackground {
    +    method @ColorInt public int getColor();
    +  }
    +
    +  public final class DividerAttributes {
    +    method @ColorInt public int getDividerColor();
    +    method public int getDividerType();
    +    method public float getPrimaryMaxRatio();
    +    method public float getPrimaryMinRatio();
    +    method @Dimension public int getWidthDp();
    +    method public boolean isDraggingToFullscreenAllowed();
    +    field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
    +    field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
    +    field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
    +    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
    +  }
    +
    +  public static final class DividerAttributes.Builder {
    +    ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
    +    ctor public DividerAttributes.Builder(int);
    +    method public androidx.window.extensions.embedding.DividerAttributes build();
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
    +  }
    +
    +  public class EmbeddedActivityWindowInfo {
    +    method public android.app.Activity getActivity();
    +    method public android.graphics.Rect getActivityStackBounds();
    +    method public android.graphics.Rect getTaskBounds();
    +    method public boolean isEmbedded();
    +  }
    +
       public abstract class EmbeddingRule {
         method public String? getTag();
       }
     
    -  public class SplitAttributes {
    +  public class ParentContainerInfo {
    +    method public android.content.res.Configuration getConfiguration();
    +    method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
    +    method public android.view.WindowMetrics getWindowMetrics();
    +  }
    +
    +  public final class SplitAttributes {
    +    method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
    +    method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
         method public int getLayoutDirection();
         method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
    +    method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
       }
     
       public static final class SplitAttributes.Builder {
         ctor public SplitAttributes.Builder();
    +    ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
         method public androidx.window.extensions.embedding.SplitAttributes build();
    +    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
    +    method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
         method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
         method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
    +    method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
       }
     
       public static final class SplitAttributes.LayoutDirection {
    @@ -139,8 +238,13 @@
         method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
         method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
         method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
    +    method public androidx.window.extensions.embedding.SplitInfo.Token getSplitInfoToken();
         method @Deprecated public float getSplitRatio();
    -    method public android.os.IBinder getToken();
    +    method @Deprecated public android.os.IBinder getToken();
    +  }
    +
    +  public static final class SplitInfo.Token {
    +    method public static androidx.window.extensions.embedding.SplitInfo.Token createFromBinder(android.os.IBinder);
       }
     
       public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
    @@ -166,6 +270,17 @@
         method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
       }
     
    +  public class SplitPinRule extends androidx.window.extensions.embedding.SplitRule {
    +    method public boolean isSticky();
    +  }
    +
    +  public static final class SplitPinRule.Builder {
    +    ctor public SplitPinRule.Builder(androidx.window.extensions.embedding.SplitAttributes, androidx.window.extensions.core.util.function.Predicate);
    +    method public androidx.window.extensions.embedding.SplitPinRule build();
    +    method public androidx.window.extensions.embedding.SplitPinRule.Builder setSticky(boolean);
    +    method public androidx.window.extensions.embedding.SplitPinRule.Builder setTag(String);
    +  }
    +
       public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
         method public int getFinishPrimaryWithPlaceholder();
         method @Deprecated public int getFinishPrimaryWithSecondary();
    @@ -198,6 +313,13 @@
         field public static final int FINISH_NEVER = 0; // 0x0
       }
     
    +  public final class WindowAttributes {
    +    ctor public WindowAttributes(int);
    +    method public int getDimAreaBehavior();
    +    field public static final int DIM_AREA_ON_ACTIVITY_STACK = 1; // 0x1
    +    field public static final int DIM_AREA_ON_TASK = 2; // 0x2
    +  }
    +
     }
     
     package androidx.window.extensions.layout {
    @@ -206,6 +328,24 @@
         method public android.graphics.Rect getBounds();
       }
     
    +  public final class DisplayFoldFeature {
    +    method public int getType();
    +    method public boolean hasProperties(int...);
    +    method public boolean hasProperty(int);
    +    field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
    +    field public static final int TYPE_HINGE = 1; // 0x1
    +    field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
    +    field public static final int TYPE_UNKNOWN = 0; // 0x0
    +  }
    +
    +  public static final class DisplayFoldFeature.Builder {
    +    ctor public DisplayFoldFeature.Builder(int);
    +    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
    +    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
    +    method public androidx.window.extensions.layout.DisplayFoldFeature build();
    +    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
    +  }
    +
       public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
         ctor public FoldingFeature(android.graphics.Rect, int, int);
         method public android.graphics.Rect getBounds();
    @@ -217,9 +357,19 @@
         field public static final int TYPE_HINGE = 2; // 0x2
       }
     
    +  public final class SupportedWindowFeatures {
    +    method public java.util.List getDisplayFoldFeatures();
    +  }
    +
    +  public static final class SupportedWindowFeatures.Builder {
    +    ctor public SupportedWindowFeatures.Builder(java.util.List);
    +    method public androidx.window.extensions.layout.SupportedWindowFeatures build();
    +  }
    +
       public interface WindowLayoutComponent {
         method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer);
         method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer);
    +    method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
         method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer);
         method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer);
       }
    
    diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
    index 8bf868f..563cab4 100644
    --- a/window/extensions/extensions/api/restricted_current.txt
    +++ b/window/extensions/extensions/api/restricted_current.txt
    
    @@ -52,17 +52,36 @@
     package androidx.window.extensions.embedding {
     
       public interface ActivityEmbeddingComponent {
    +    method public default void clearActivityStackAttributesCalculator();
    +    method public default void clearEmbeddedActivityWindowInfoCallback();
         method public void clearSplitAttributesCalculator();
         method public void clearSplitInfoCallback();
    -    method public default void finishActivityStacks(java.util.Set);
    +    method @Deprecated public default void finishActivityStacks(java.util.Set);
    +    method public default void finishActivityStacksWithTokens(java.util.Set);
    +    method public default androidx.window.extensions.embedding.ActivityStack.Token? getActivityStackToken(String);
    +    method public default androidx.window.extensions.embedding.EmbeddedActivityWindowInfo? getEmbeddedActivityWindowInfo(android.app.Activity);
    +    method public default androidx.window.extensions.embedding.ParentContainerInfo? getParentContainerInfo(androidx.window.extensions.embedding.ActivityStack.Token);
         method public default void invalidateTopVisibleSplitAttributes();
         method public boolean isActivityEmbedded(android.app.Activity);
    +    method public default boolean pinTopActivityStack(int, androidx.window.extensions.embedding.SplitPinRule);
    +    method public default void registerActivityStackCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer!>);
    +    method public default void setActivityStackAttributesCalculator(androidx.window.extensions.core.util.function.Function);
    +    method public default void setEmbeddedActivityWindowInfoCallback(java.util.concurrent.Executor, androidx.window.extensions.core.util.function.Consumer);
         method public void setEmbeddingRules(java.util.Set);
    -    method public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
    +    method @Deprecated public default android.app.ActivityOptions setLaunchingActivityStack(android.app.ActivityOptions, android.os.IBinder);
         method public void setSplitAttributesCalculator(androidx.window.extensions.core.util.function.Function);
         method public default void setSplitInfoCallback(androidx.window.extensions.core.util.function.Consumer!>);
         method @Deprecated public void setSplitInfoCallback(java.util.function.Consumer!>);
    -    method public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
    +    method public default void unpinTopActivityStack(int);
    +    method public default void unregisterActivityStackCallback(androidx.window.extensions.core.util.function.Consumer!>);
    +    method public default void updateActivityStackAttributes(androidx.window.extensions.embedding.ActivityStack.Token, androidx.window.extensions.embedding.ActivityStackAttributes);
    +    method @Deprecated public default void updateSplitAttributes(android.os.IBinder, androidx.window.extensions.embedding.SplitAttributes);
    +    method public default void updateSplitAttributes(androidx.window.extensions.embedding.SplitInfo.Token, androidx.window.extensions.embedding.SplitAttributes);
    +  }
    +
    +  public class ActivityEmbeddingOptionsProperties {
    +    field public static final String KEY_ACTIVITY_STACK_TOKEN = "androidx.window.extensions.embedding.ActivityStackToken";
    +    field public static final String KEY_OVERLAY_TAG = "androidx.window.extensions.embedding.OverlayTag";
       }
     
       public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
    @@ -81,23 +100,104 @@
     
       public class ActivityStack {
         method public java.util.List getActivities();
    +    method public androidx.window.extensions.embedding.ActivityStack.Token getActivityStackToken();
    +    method public String? getTag();
    +    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.os.IBinder getToken();
         method public boolean isEmpty();
       }
     
    +  public static final class ActivityStack.Token {
    +    method public static androidx.window.extensions.embedding.ActivityStack.Token createFromBinder(android.os.IBinder);
    +    method public static androidx.window.extensions.embedding.ActivityStack.Token readFromBundle(android.os.Bundle);
    +    method public android.os.Bundle toBundle();
    +    field public static final androidx.window.extensions.embedding.ActivityStack.Token INVALID_ACTIVITY_STACK_TOKEN;
    +  }
    +
    +  public final class ActivityStackAttributes {
    +    method public android.graphics.Rect getRelativeBounds();
    +    method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
    +  }
    +
    +  public static final class ActivityStackAttributes.Builder {
    +    ctor public ActivityStackAttributes.Builder();
    +    method public androidx.window.extensions.embedding.ActivityStackAttributes build();
    +    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setRelativeBounds(android.graphics.Rect);
    +    method public androidx.window.extensions.embedding.ActivityStackAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
    +  }
    +
    +  public class ActivityStackAttributesCalculatorParams {
    +    method public String getActivityStackTag();
    +    method public android.os.Bundle getLaunchOptions();
    +    method public androidx.window.extensions.embedding.ParentContainerInfo getParentContainerInfo();
    +  }
    +
    +  public abstract class AnimationBackground {
    +    method public static androidx.window.extensions.embedding.AnimationBackground.ColorBackground createColorBackground(@ColorInt int);
    +    field public static final androidx.window.extensions.embedding.AnimationBackground ANIMATION_BACKGROUND_DEFAULT;
    +  }
    +
    +  public static class AnimationBackground.ColorBackground extends androidx.window.extensions.embedding.AnimationBackground {
    +    method @ColorInt public int getColor();
    +  }
    +
    +  public final class DividerAttributes {
    +    method @ColorInt public int getDividerColor();
    +    method public int getDividerType();
    +    method public float getPrimaryMaxRatio();
    +    method public float getPrimaryMinRatio();
    +    method @Dimension public int getWidthDp();
    +    method public boolean isDraggingToFullscreenAllowed();
    +    field public static final int DIVIDER_TYPE_DRAGGABLE = 2; // 0x2
    +    field public static final int DIVIDER_TYPE_FIXED = 1; // 0x1
    +    field public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
    +    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
    +  }
    +
    +  public static final class DividerAttributes.Builder {
    +    ctor public DividerAttributes.Builder(androidx.window.extensions.embedding.DividerAttributes);
    +    ctor public DividerAttributes.Builder(int);
    +    method public androidx.window.extensions.embedding.DividerAttributes build();
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDividerColor(@ColorInt int);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setDraggingToFullscreenAllowed(boolean);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMaxRatio(float);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setPrimaryMinRatio(float);
    +    method public androidx.window.extensions.embedding.DividerAttributes.Builder setWidthDp(@Dimension int);
    +  }
    +
    +  public class EmbeddedActivityWindowInfo {
    +    method public android.app.Activity getActivity();
    +    method public android.graphics.Rect getActivityStackBounds();
    +    method public android.graphics.Rect getTaskBounds();
    +    method public boolean isEmbedded();
    +  }
    +
       public abstract class EmbeddingRule {
         method public String? getTag();
       }
     
    -  public class SplitAttributes {
    +  public class ParentContainerInfo {
    +    method public android.content.res.Configuration getConfiguration();
    +    method public androidx.window.extensions.layout.WindowLayoutInfo getWindowLayoutInfo();
    +    method public android.view.WindowMetrics getWindowMetrics();
    +  }
    +
    +  public final class SplitAttributes {
    +    method public androidx.window.extensions.embedding.AnimationBackground getAnimationBackground();
    +    method public androidx.window.extensions.embedding.DividerAttributes? getDividerAttributes();
         method public int getLayoutDirection();
         method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
    +    method public androidx.window.extensions.embedding.WindowAttributes getWindowAttributes();
       }
     
       public static final class SplitAttributes.Builder {
         ctor public SplitAttributes.Builder();
    +    ctor public SplitAttributes.Builder(androidx.window.extensions.embedding.SplitAttributes);
         method public androidx.window.extensions.embedding.SplitAttributes build();
    +    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.extensions.embedding.AnimationBackground);
    +    method public androidx.window.extensions.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.extensions.embedding.DividerAttributes?);
         method public androidx.window.extensions.embedding.SplitAttributes.Builder setLayoutDirection(int);
         method public androidx.window.extensions.embedding.SplitAttributes.Builder setSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
    +    method public androidx.window.extensions.embedding.SplitAttributes.Builder setWindowAttributes(androidx.window.extensions.embedding.WindowAttributes);
       }
     
       public static final class SplitAttributes.LayoutDirection {
    @@ -139,8 +239,13 @@
         method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
         method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
         method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
    +    method public androidx.window.extensions.embedding.SplitInfo.Token getSplitInfoToken();
         method @Deprecated public float getSplitRatio();
    -    method public android.os.IBinder getToken();
    +    method @Deprecated public android.os.IBinder getToken();
    +  }
    +
    +  public static final class SplitInfo.Token {
    +    method public static androidx.window.extensions.embedding.SplitInfo.Token createFromBinder(android.os.IBinder);
       }
     
       public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
    @@ -166,6 +271,17 @@
         method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
       }
     
    +  public class SplitPinRule extends androidx.window.extensions.embedding.SplitRule {
    +    method public boolean isSticky();
    +  }
    +
    +  public static final class SplitPinRule.Builder {
    +    ctor public SplitPinRule.Builder(androidx.window.extensions.embedding.SplitAttributes, androidx.window.extensions.core.util.function.Predicate);
    +    method public androidx.window.extensions.embedding.SplitPinRule build();
    +    method public androidx.window.extensions.embedding.SplitPinRule.Builder setSticky(boolean);
    +    method public androidx.window.extensions.embedding.SplitPinRule.Builder setTag(String);
    +  }
    +
       public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
         method public int getFinishPrimaryWithPlaceholder();
         method @Deprecated public int getFinishPrimaryWithSecondary();
    @@ -198,6 +314,13 @@
         field public static final int FINISH_NEVER = 0; // 0x0
       }
     
    +  public final class WindowAttributes {
    +    ctor public WindowAttributes(int);
    +    method public int getDimAreaBehavior();
    +    field public static final int DIM_AREA_ON_ACTIVITY_STACK = 1; // 0x1
    +    field public static final int DIM_AREA_ON_TASK = 2; // 0x2
    +  }
    +
     }
     
     package androidx.window.extensions.layout {
    @@ -206,6 +329,24 @@
         method public android.graphics.Rect getBounds();
       }
     
    +  public final class DisplayFoldFeature {
    +    method public int getType();
    +    method public boolean hasProperties(int...);
    +    method public boolean hasProperty(int);
    +    field public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1; // 0x1
    +    field public static final int TYPE_HINGE = 1; // 0x1
    +    field public static final int TYPE_SCREEN_FOLD_IN = 2; // 0x2
    +    field public static final int TYPE_UNKNOWN = 0; // 0x0
    +  }
    +
    +  public static final class DisplayFoldFeature.Builder {
    +    ctor public DisplayFoldFeature.Builder(int);
    +    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperties(int...);
    +    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder addProperty(int);
    +    method public androidx.window.extensions.layout.DisplayFoldFeature build();
    +    method public androidx.window.extensions.layout.DisplayFoldFeature.Builder clearProperties();
    +  }
    +
       public class FoldingFeature implements androidx.window.extensions.layout.DisplayFeature {
         ctor public FoldingFeature(android.graphics.Rect, int, int);
         method public android.graphics.Rect getBounds();
    @@ -217,9 +358,19 @@
         field public static final int TYPE_HINGE = 2; // 0x2
       }
     
    +  public final class SupportedWindowFeatures {
    +    method public java.util.List getDisplayFoldFeatures();
    +  }
    +
    +  public static final class SupportedWindowFeatures.Builder {
    +    ctor public SupportedWindowFeatures.Builder(java.util.List);
    +    method public androidx.window.extensions.layout.SupportedWindowFeatures build();
    +  }
    +
       public interface WindowLayoutComponent {
         method @Deprecated public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer);
         method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, androidx.window.extensions.core.util.function.Consumer);
    +    method public default androidx.window.extensions.layout.SupportedWindowFeatures getSupportedWindowFeatures();
         method public default void removeWindowLayoutInfoListener(androidx.window.extensions.core.util.function.Consumer);
         method @Deprecated public void removeWindowLayoutInfoListener(java.util.function.Consumer);
       }
    
    diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
    index 611e695..d2fc989 100644
    --- a/window/extensions/extensions/build.gradle
    +++ b/window/extensions/extensions/build.gradle
    
    @@ -38,10 +38,12 @@
         testImplementation(libs.testExtJunit)
         testImplementation(libs.testRunner)
         testImplementation(libs.testRules)
    +    testImplementation(libs.truth)
     
         androidTestImplementation(libs.testExtJunit)
         androidTestImplementation(libs.testRunner)
         androidTestImplementation(libs.testRules)
    +    androidTestImplementation(libs.truth)
         androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
         androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
     }
    
    diff --git a/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java
    new file mode 100644
    index 0000000..953ace1
    --- /dev/null
    +++ b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/ActivityStackAttributesTest.java
    
    @@ -0,0 +1,77 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;
    +import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
    +
    +import static com.google.common.truth.Truth.assertThat;
    +
    +import android.graphics.Rect;
    +
    +import org.junit.Test;
    +
    +/**
    + * Verifies {@link ActivityStackAttributes} behavior.
    + */
    +public class ActivityStackAttributesTest {
    +
    +    @Test
    +    public void testActivityStackAttributesDefaults() {
    +        final ActivityStackAttributes defaultAttrs = new ActivityStackAttributes.Builder().build();
    +        assertThat(defaultAttrs.getRelativeBounds().isEmpty()).isTrue();
    +        assertThat(defaultAttrs.getWindowAttributes().getDimAreaBehavior())
    +                .isEqualTo(DIM_AREA_ON_ACTIVITY_STACK);
    +    }
    +
    +    @Test
    +    public void testActivityStackAttributesEqualsMatchHashCode() {
    +        final ActivityStackAttributes attrs1 = new ActivityStackAttributes.Builder()
    +                .setRelativeBounds(new Rect(0, 0, 10, 10))
    +                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
    +                .build();
    +
    +        final ActivityStackAttributes attrs2 = new ActivityStackAttributes.Builder()
    +                .setRelativeBounds(new Rect(0, 0, 10, 10))
    +                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
    +                .build();
    +
    +        final ActivityStackAttributes attrs3 = new ActivityStackAttributes.Builder()
    +                .setRelativeBounds(new Rect(10, 0, 20, 10))
    +                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
    +                .build();
    +
    +        final ActivityStackAttributes attrs4 = new ActivityStackAttributes.Builder()
    +                .setRelativeBounds(new Rect(10, 0, 20, 10))
    +                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
    +                .build();
    +
    +        final ActivityStackAttributes attrs5 = new ActivityStackAttributes.Builder()
    +                .setRelativeBounds(new Rect(0, 0, 10, 10))
    +                .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK))
    +                .build();
    +
    +        assertThat(attrs1).isNotEqualTo(attrs2);
    +        assertThat(attrs1.hashCode()).isNotEqualTo(attrs2.hashCode());
    +        assertThat(attrs1).isNotEqualTo(attrs3);
    +        assertThat(attrs1.hashCode()).isNotEqualTo(attrs3.hashCode());
    +        assertThat(attrs1).isNotEqualTo(attrs4);
    +        assertThat(attrs1.hashCode()).isNotEqualTo(attrs4.hashCode());
    +        assertThat(attrs1).isEqualTo(attrs5);
    +        assertThat(attrs1.hashCode()).isEqualTo(attrs5.hashCode());
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java
    new file mode 100644
    index 0000000..f699b61
    --- /dev/null
    +++ b/window/extensions/extensions/src/androidTest/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfoTest.java
    
    @@ -0,0 +1,102 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotEquals;
    +import static org.junit.Assert.assertTrue;
    +
    +import android.app.Activity;
    +import android.graphics.Rect;
    +
    +import androidx.test.ext.junit.runners.AndroidJUnit4;
    +import androidx.test.filters.SmallTest;
    +
    +import org.junit.Before;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.mockito.Mock;
    +import org.mockito.MockitoAnnotations;
    +
    +/** Tests for {@link EmbeddedActivityWindowInfo} class. */
    +@SmallTest
    +@RunWith(AndroidJUnit4.class)
    +public class EmbeddedActivityWindowInfoTest {
    +
    +    @Mock
    +    private Activity mActivity;
    +    @Mock
    +    private Activity mActivity2;
    +
    +    @Before
    +    public void setUp() {
    +        MockitoAnnotations.initMocks(this);
    +    }
    +
    +    @Test
    +    public void testGetter() {
    +        final Rect taskBounds = new Rect(0, 0, 1000, 2000);
    +        final Rect activityStackBounds = new Rect(0, 0, 1000, 1000);
    +        final EmbeddedActivityWindowInfo info = new EmbeddedActivityWindowInfo(mActivity,
    +                true /* isEmbedded */, taskBounds, activityStackBounds);
    +
    +        assertEquals(mActivity, info.getActivity());
    +        assertTrue(info.isEmbedded());
    +        assertEquals(taskBounds, info.getTaskBounds());
    +        assertEquals(activityStackBounds, info.getActivityStackBounds());
    +    }
    +
    +    @Test
    +    public void testEqualsAndHashCode() {
    +        final EmbeddedActivityWindowInfo info1 = new EmbeddedActivityWindowInfo(mActivity,
    +                true /* isEmbedded */,
    +                new Rect(0, 0, 1000, 2000),
    +                new Rect(0, 0, 1000, 1000));
    +        final EmbeddedActivityWindowInfo info2 = new EmbeddedActivityWindowInfo(mActivity2,
    +                true /* isEmbedded */,
    +                new Rect(0, 0, 1000, 2000),
    +                new Rect(0, 0, 1000, 1000));
    +        final EmbeddedActivityWindowInfo info3 = new EmbeddedActivityWindowInfo(mActivity,
    +                false /* isEmbedded */,
    +                new Rect(0, 0, 1000, 2000),
    +                new Rect(0, 0, 1000, 1000));
    +        final EmbeddedActivityWindowInfo info4 = new EmbeddedActivityWindowInfo(mActivity,
    +                true /* isEmbedded */,
    +                new Rect(0, 0, 1000, 1000),
    +                new Rect(0, 0, 1000, 1000));
    +        final EmbeddedActivityWindowInfo info5 = new EmbeddedActivityWindowInfo(mActivity,
    +                true /* isEmbedded */,
    +                new Rect(0, 0, 1000, 2000),
    +                new Rect(0, 0, 1000, 1500));
    +        final EmbeddedActivityWindowInfo info6 = new EmbeddedActivityWindowInfo(mActivity,
    +                true /* isEmbedded */,
    +                new Rect(0, 0, 1000, 2000),
    +                new Rect(0, 0, 1000, 1000));
    +
    +        assertNotEquals(info1, info2);
    +        assertNotEquals(info1, info3);
    +        assertNotEquals(info1, info4);
    +        assertNotEquals(info1, info5);
    +        assertEquals(info1, info6);
    +
    +        assertNotEquals(info1.hashCode(), info2.hashCode());
    +        assertNotEquals(info1.hashCode(), info3.hashCode());
    +        assertNotEquals(info1.hashCode(), info4.hashCode());
    +        assertNotEquals(info1.hashCode(), info5.hashCode());
    +        assertEquals(info1.hashCode(), info6.hashCode());
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
    index 8ad2a9c..2ccad2d 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
    
    @@ -161,6 +161,8 @@
         void startRearDisplaySession(@NonNull Activity activity,
                 @NonNull Consumer<@WindowAreaSessionState Integer> consumer);
     
    +    // TODO(b/264546746): Remove deprecated Window Extensions APIs after apps in g3 is updated to
    +    // the latest library.
         /**
          * @deprecated Use {@link #startRearDisplaySession(Activity, Consumer)}.
          */
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
    index b582e92..13b64d2 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
    
    @@ -22,13 +22,17 @@
     import android.view.WindowMetrics;
     
     import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
     import androidx.window.extensions.RequiresVendorApiLevel;
     import androidx.window.extensions.WindowExtensions;
     import androidx.window.extensions.core.util.function.Consumer;
     import androidx.window.extensions.core.util.function.Function;
    +import androidx.window.extensions.util.SetCompat;
    +
     
     import java.util.List;
     import java.util.Set;
    +import java.util.concurrent.Executor;
     
     /**
      * Extension component definition that is used by the WindowManager library to trigger custom
    @@ -57,8 +61,7 @@
         /**
          * Sets the callback that notifies WM Jetpack about changes in split states from the Extensions
          * Sidecar implementation. The listener should be registered for the lifetime of the process.
    -     * There are no threading guarantees where the events are dispatched from. All messages are
    -     * re-posted to the executors provided by developers.
    +     * There are no threading guarantees where the events are dispatched from.
          *
          * @param consumer the callback to notify {@link SplitInfo} list changes
          */
    @@ -86,6 +89,50 @@
         boolean isActivityEmbedded(@NonNull Activity activity);
     
         /**
    +     * Pins the top-most {@link ActivityStack} to keep the stack of the Activities to be
    +     * positioned on top. The rest of the activities in the Task will be split with the pinned
    +     * {@link ActivityStack}. The pinned {@link ActivityStack} would also have isolated activity
    +     * navigation in which only the activities that are started from the pinned
    +     * {@link ActivityStack} can be added on top of the {@link ActivityStack}.
    +     * 

    + * The pinned {@link ActivityStack} is unpinned whenever the parent Task bounds don't + * satisfy the dimensions and aspect ratio requirements {@link SplitRule#checkParentMetrics} + * to show two {@link ActivityStack}s. See {@link SplitPinRule.Builder#setSticky} if + * the same {@link ActivityStack} should be pinned again whenever the parent Task bounds + * satisfies the dimensions and aspect ratios requirements defined in the rule. + * + * @param taskId The id of the Task that top {@link ActivityStack} should be pinned. + * @param splitPinRule The SplitRule that specifies how the top {@link ActivityStack} should + * be split with others. + * @return Returns {@code true} if the top {@link ActivityStack} is successfully pinned. + * Otherwise, {@code false}. Few examples are: + * 1. There's no {@link ActivityStack}. + * 2. There is already an existing pinned {@link ActivityStack}. + * 3. There's no other {@link ActivityStack} to split with the top + * {@link ActivityStack}. + */ + @RequiresVendorApiLevel(level = 5) + default boolean pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Unpins the pinned {@link ActivityStack}. The {@link ActivityStack} will still be the + * top-most {@link ActivityStack} right after unpinned, and the {@link ActivityStack} could + * be expanded or continue to be split with the next top {@link ActivityStack} if the current + * state matches any of the existing {@link SplitPairRule}. It is a no-op call if the task + * does not have a pinned {@link ActivityStack}. + * + * @param taskId The id of the Task that top {@link ActivityStack} should be unpinned. + */ + @RequiresVendorApiLevel(level = 5) + default void unpinTopActivityStack(int taskId) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** * Sets a function to compute the {@link SplitAttributes} for the {@link SplitRule} and current * window state provided in {@link SplitAttributesCalculatorParams}. *

    @@ -127,12 +174,10 @@ void clearSplitAttributesCalculator(); /** - * Sets the launching {@link ActivityStack} to the given {@link ActivityOptions}. - * - * @param options The {@link ActivityOptions} to be updated. - * @param token The {@link ActivityStack#getToken()} to represent the {@link ActivityStack} + * @deprecated Use {@link ActivityEmbeddingOptionsProperties#KEY_ACTIVITY_STACK_TOKEN} instead. */ - @RequiresVendorApiLevel(level = 3) + @Deprecated + @RequiresVendorApiLevel(level = 3, deprecatedSince = 5) @NonNull default ActivityOptions setLaunchingActivityStack(@NonNull ActivityOptions options, @NonNull IBinder token) { @@ -141,6 +186,16 @@ } /** + * @deprecated Use {@link #finishActivityStacksWithTokens(Set)} with instead. + */ + @Deprecated + @RequiresVendorApiLevel(level = 3, deprecatedSince = 5) + default void finishActivityStacks(@NonNull Set activityStackTokens) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** * Finishes a set of {@link ActivityStack}s. When an {@link ActivityStack} that was in an active * split is finished, the other {@link ActivityStack} in the same {@link SplitInfo} can be * expanded to fill the parent task container. @@ -148,10 +203,16 @@ * @param activityStackTokens The set of tokens of {@link ActivityStack}-s that is going to be * finished. */ - @RequiresVendorApiLevel(level = 3) - default void finishActivityStacks(@NonNull Set activityStackTokens) { - throw new UnsupportedOperationException("This method must not be called unless there is a" - + " corresponding override implementation on the device."); + @SuppressWarnings("deprecation") // Use finishActivityStacks(Set) as its core implementation. + @RequiresVendorApiLevel(level = 5) + default void finishActivityStacksWithTokens( + @NonNull Set activityStackTokens) { + final Set binderSet = SetCompat.create(); + + for (ActivityStack.Token token : activityStackTokens) { + binderSet.add(token.getRawToken()); + } + finishActivityStacks(binderSet); } /** @@ -169,15 +230,215 @@ } /** + * @deprecated Use {@link #updateSplitAttributes(SplitInfo.Token, SplitAttributes)} instead. + */ + @Deprecated + @RequiresVendorApiLevel(level = 3, deprecatedSince = 5) + default void updateSplitAttributes(@NonNull IBinder splitInfoToken, + @NonNull SplitAttributes splitAttributes) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** * Updates the {@link SplitAttributes} of a split pair. This is an alternative to using * a split attributes calculator callback, applicable when apps only need to update the * splits in a few cases but rely on the default split attributes otherwise. * @param splitInfoToken The identifier of the split pair to update. * @param splitAttributes The {@link SplitAttributes} to apply to the split pair. */ - @RequiresVendorApiLevel(level = 3) - default void updateSplitAttributes(@NonNull IBinder splitInfoToken, + @SuppressWarnings("deprecation") // Use finishActivityStacks(Set). + @RequiresVendorApiLevel(level = 5) + default void updateSplitAttributes(@NonNull SplitInfo.Token splitInfoToken, @NonNull SplitAttributes splitAttributes) { + updateSplitAttributes(splitInfoToken.getRawToken(), splitAttributes); + } + + /** + * Returns the {@link ParentContainerInfo} by the {@link ActivityStack} token, or {@code null} + * if there's not such {@link ActivityStack} associated with the {@code token}. + * + * @param activityStackToken the token of an {@link ActivityStack}. + */ + @RequiresVendorApiLevel(level = 6) + @Nullable + default ParentContainerInfo getParentContainerInfo( + @NonNull ActivityStack.Token activityStackToken) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Sets a function to compute the {@link ActivityStackAttributes} for the ActivityStack given + * for the current window and device state provided in + * {@link ActivityStackAttributesCalculatorParams} on the main thread. + *

    + * This calculator function is only triggered if the {@link ActivityStack#getTag()} is + * specified. Similar to {@link #setSplitAttributesCalculator(Function)}, the calculator + * function could be triggered multiple times. It will be triggered whenever there's a + * launching standalone {@link ActivityStack} with {@link ActivityStack#getTag()} specified, + * or a parent window or device state update, such as device rotation, folding state change, + * or the host task goes to multi-window mode. + * + * @param calculator The calculator function to calculate {@link ActivityStackAttributes} based + * on {@link ActivityStackAttributesCalculatorParams}. + */ + @RequiresVendorApiLevel(level = 6) + default void setActivityStackAttributesCalculator(@NonNull Function< + ActivityStackAttributesCalculatorParams, ActivityStackAttributes> calculator) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Clears the calculator function previously set by + * {@link #setActivityStackAttributesCalculator(Function)} + */ + @RequiresVendorApiLevel(level = 6) + default void clearActivityStackAttributesCalculator() { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Updates {@link ActivityStackAttributes} to an {@link ActivityStack} specified with + * {@code token} and applies the change directly. If there's no such an {@link ActivityStack}, + * this method is no-op. + * + * @param token The {@link ActivityStack} to update. + * @param activityStackAttributes The attributes to be applied + */ + @RequiresVendorApiLevel(level = 6) + default void updateActivityStackAttributes(@NonNull ActivityStack.Token token, + @NonNull ActivityStackAttributes activityStackAttributes) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Gets the {@link ActivityStack}'s token by {@code tag}, or {@code null} if there's no + * {@link ActivityStack} associated with the {@code tag}. For example, the {@link ActivityStack} + * is dismissed before the is method is called. + *

    + * The {@link ActivityStack} token can be obtained immediately after the {@link ActivityStack} + * is created. This method is usually used when Activity Embedding library wants to + * {@link #updateActivityStackAttributes} before receiving + * the {@link ActivityStack} record from the callback set by + * {@link #registerActivityStackCallback}. + *

    + * For example, an app launches an overlay container and calls + * {@link #updateActivityStackAttributes} immediately right before the overlay + * {@link ActivityStack} is received from {@link #registerActivityStackCallback}. + * + * @param tag A unique identifier of an {@link ActivityStack} if set + * @return The {@link ActivityStack}'s token that the tag is associated with, or {@code null} + * if there's no such an {@link ActivityStack}. + */ + @RequiresVendorApiLevel(level = 6) + @Nullable + default ActivityStack.Token getActivityStackToken(@NonNull String tag) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Registers a callback that notifies WindowManager Jetpack about changes in + * {@link ActivityStack}. + *

    + * In most cases, {@link ActivityStack} are a part of {@link SplitInfo} as + * {@link SplitInfo#getPrimaryActivityStack() the primary ActivityStack} or + * {@link SplitInfo#getSecondaryActivityStack() the secondary ActivityStack} of a + * {@link SplitInfo}. + *

    + * However, there are some cases that {@link ActivityStack} is standalone and usually + * expanded. Cases are: + *

      + *
    • A started {@link Activity} matches {@link ActivityRule} with + * {@link ActivityRule#shouldAlwaysExpand()} {@code true}. + * + *
    • The {@code ActivityStack} is an overlay {@code ActivityStack}. + * + *
    • The associated {@link ActivityStack activityStacks} of a {@code ActivityStack} are + * dismissed by {@link #finishActivityStacks(Set)}. + * + *
    • One {@link ActivityStack} of {@link SplitInfo}(Either + * {@link SplitInfo#getPrimaryActivityStack() the primary ActivityStack} or + * {@link SplitInfo#getSecondaryActivityStack() the secondary ActivityStack}) is + * empty and finished, while the other {@link ActivityStack} is not finished with the + * finishing {@link ActivityStack}. + *

      + * An example is a pair of activities matches a {@link SplitPairRule}, and its + * {@link SplitPairRule#getFinishPrimaryWithSecondary()} is {@link SplitRule#FINISH_NEVER}. + * Then if the last activity of + * {@link SplitInfo#getSecondaryActivityStack() the secondary ActivityStack}) is finished, + * {@link SplitInfo#getPrimaryActivityStack() the primary ActivityStack} will still remain. + *

    + * + * @param executor the executor to dispatch {@link ActivityStack} list changes. + * @param callback the callback to notify {@link ActivityStack} list changes. + * + * @see ActivityEmbeddingComponent#finishActivityStacks(Set) + */ + @RequiresVendorApiLevel(level = 5) + default void registerActivityStackCallback(@NonNull Executor executor, + @NonNull Consumer> callback) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Removes the callback previously registered in {@link #registerActivityStackCallback}, or + * no-op if the callback hasn't been registered yet. + * + * @param callback The callback to remove, which should have been registered. + */ + @RequiresVendorApiLevel(level = 5) + default void unregisterActivityStackCallback( + @NonNull Consumer> callback) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Sets a callback that notifies WindowManager Jetpack about changes for a given + * {@link Activity} to its {@link EmbeddedActivityWindowInfo}. + *

    + * The callback will be invoked when the {@link EmbeddedActivityWindowInfo} is changed after + * the {@link Activity} is launched. Similar to {@link Activity#onConfigurationChanged}, the + * callback will only be invoked for visible {@link Activity}. + * + * @param executor the executor to dispatch {@link EmbeddedActivityWindowInfo} change. + * @param callback the callback to notify {@link EmbeddedActivityWindowInfo} change. + */ + @RequiresVendorApiLevel(level = 6) + default void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor, + @NonNull Consumer callback) { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Clears the callback previously set by + * {@link #setEmbeddedActivityWindowInfoCallback(Executor, Consumer)} + */ + @RequiresVendorApiLevel(level = 6) + default void clearEmbeddedActivityWindowInfoCallback() { + throw new UnsupportedOperationException("This method must not be called unless there is a" + + " corresponding override implementation on the device."); + } + + /** + * Returns the {@link EmbeddedActivityWindowInfo} of the given {@link Activity}, or + * {@code null} if the {@link Activity} is not attached. + *

    + * This API can be used when {@link #setEmbeddedActivityWindowInfoCallback} is not set before + * the Activity is attached. + * + * @param activity the {@link Activity} to get {@link EmbeddedActivityWindowInfo} for. + */ + @RequiresVendorApiLevel(level = 6) + @Nullable + default EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) { throw new UnsupportedOperationException("This method must not be called unless there is a" + " corresponding override implementation on the device."); }

    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
    new file mode 100644
    index 0000000..fce31d8
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingOptionsProperties.java
    
    @@ -0,0 +1,56 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.content.Intent;
    +import android.os.Bundle;
    +import android.os.IBinder;
    +
    +/**
    + * A class that contains activity embedding properties that puts to or retrieves from
    + * {@link android.app.ActivityOptions}.
    + */
    +public class ActivityEmbeddingOptionsProperties {
    +
    +    private ActivityEmbeddingOptionsProperties() {}
    +
    +    /**
    +     * The key of the unique identifier that put into {@link android.app.ActivityOptions}.
    +     * 

    + * Type: {@link android.os.Bundle#putString(String, String) String} + *

    + * An {@code OverlayCreateParams} property that represents the unique identifier of the overlay + * container. + */ + public static final String KEY_OVERLAY_TAG = + "androidx.window.extensions.embedding.OverlayTag"; + + /** + * The key of {@link ActivityStack.Token#toBundle()} that put into + * {@link android.app.ActivityOptions}. + *

    + * Type: {@link Bundle#putBundle} + *

    + * Apps can launch an activity into the {@link ActivityStack} that associated with + * {@code token} by {@link android.app.Activity#startActivity(Intent, Bundle)}. + * + * @see androidx.window.extensions.embedding.ActivityStack.Token#toBundle() + * @see androidx.window.extensions.embedding.ActivityStack.Token#createFromBinder(IBinder) + */ + public static final String KEY_ACTIVITY_STACK_TOKEN = + "androidx.window.extensions.embedding.ActivityStackToken"; +}

    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
    index c741647..669923f 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
    
    @@ -24,6 +24,7 @@
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import androidx.annotation.RequiresApi;
    +import androidx.window.extensions.RequiresVendorApiLevel;
     import androidx.window.extensions.WindowExtensions;
     import androidx.window.extensions.core.util.function.Predicate;
     
    @@ -107,8 +108,8 @@
              *                         matches the rule
              * @param intentPredicate the {@link Predicate} to verify if a given {@link Intent}
              *                         matches the rule
    -         * Since {@link WindowExtensions#VENDOR_API_LEVEL_2}
              */
    +        @RequiresVendorApiLevel(level = 2)
             public Builder(@NonNull Predicate activityPredicate,
                     @NonNull Predicate intentPredicate) {
                 mActivityPredicate = activityPredicate;
    @@ -124,8 +125,8 @@
     
             /**
              * @see ActivityRule#getTag()
    -         * Since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
              */
    +        @RequiresVendorApiLevel(level = 2)
             @NonNull
             public Builder setTag(@NonNull String tag) {
                 mTag = Objects.requireNonNull(tag);
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
    index 5f79d3a..1e3d4df 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
    
    @@ -17,10 +17,14 @@
     package androidx.window.extensions.embedding;
     
     import android.app.Activity;
    +import android.os.Binder;
    +import android.os.Bundle;
     import android.os.IBinder;
     
     import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
     import androidx.annotation.RestrictTo;
    +import androidx.window.extensions.RequiresVendorApiLevel;
     
     import java.util.ArrayList;
     import java.util.List;
    @@ -38,7 +42,10 @@
         private final boolean mIsEmpty;
     
         @NonNull
    -    private final IBinder mToken;
    +    private final Token mToken;
    +
    +    @Nullable
    +    private final String mTag;
     
         /**
          * The {@code ActivityStack} constructor
    @@ -48,13 +55,18 @@
          * @param isEmpty Indicates whether there's any {@link Activity} running in this
          *                {@code ActivityStack}
          * @param token The token to identify this {@code ActivityStack}
    +     * @param tag A unique identifier of {@link ActivityStack}. Only specifies for the overlay
    +     *            standalone {@link ActivityStack} currently.
          */
    -    ActivityStack(@NonNull List activities, boolean isEmpty, @NonNull IBinder token) {
    +    ActivityStack(@NonNull List activities, boolean isEmpty, @NonNull Token token,
    +            @Nullable String tag) {
             Objects.requireNonNull(activities);
             Objects.requireNonNull(token);
    +
             mActivities = new ArrayList<>(activities);
             mIsEmpty = isEmpty;
             mToken = token;
    +        mTag = tag;
         }
     
         /**
    @@ -86,14 +98,35 @@
     
         /**
          * Returns a token uniquely identifying the container.
    -     * Since {@link WindowExtensions#VENDOR_API_LEVEL_3}
          */
    -    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    @RequiresVendorApiLevel(level = 5)
         @NonNull
    -    public IBinder getToken() {
    +    public Token getActivityStackToken() {
             return mToken;
         }
     
    +    // TODO(b/329997430): Remove it after there's no more usages.
    +    /**
    +     * @deprecated Use {@link #getActivityStackToken()} instead. Use this method only if
    +     * {@link #getActivityStackToken()} cannot be used.
    +     */
    +    @RequiresVendorApiLevel(level = 5, deprecatedSince = 5)
    +    @Deprecated
    +    @NonNull
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    +    public IBinder getToken() {
    +        return mToken.getRawToken();
    +    }
    +
    +    /**
    +     * Returns the associated tag if specified. Otherwise, returns {@code null}.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @Nullable
    +    public String getTag() {
    +        return mTag;
    +    }
    +
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
    @@ -101,7 +134,8 @@
             ActivityStack that = (ActivityStack) o;
             return mActivities.equals(that.mActivities)
                     && mIsEmpty == that.mIsEmpty
    -                && mToken.equals(that.mToken);
    +                && mToken.equals(that.mToken)
    +                && Objects.equals(mTag, that.mTag);
         }
     
         @Override
    @@ -109,6 +143,8 @@
             int result = (mIsEmpty ? 1 : 0);
             result = result * 31 + mActivities.hashCode();
             result = result * 31 + mToken.hashCode();
    +        result = result * 31 + Objects.hashCode(mTag);
    +
             return result;
         }
     
    @@ -118,6 +154,95 @@
             return "ActivityStack{" + "mActivities=" + mActivities
                     + ", mIsEmpty=" + mIsEmpty
                     + ", mToken=" + mToken
    +                + ", mTag=" + mTag
                     + '}';
         }
    +
    +    /**
    +     * A unique identifier to represent an {@link ActivityStack}.
    +     */
    +    public static final class Token {
    +
    +        /**
    +         * An invalid token to provide compatibility value before vendor API level 5.
    +         */
    +        @NonNull
    +        public static final Token INVALID_ACTIVITY_STACK_TOKEN = new Token(new Binder());
    +
    +        private static final String KEY_ACTIVITY_STACK_RAW_TOKEN = "androidx.window.extensions"
    +                + ".embedding.ActivityStack.Token";
    +
    +        private final IBinder mToken;
    +
    +        Token(@NonNull IBinder token) {
    +            mToken = token;
    +        }
    +
    +        /**
    +         * Creates an {@link ActivityStack} token from binder.
    +         *
    +         * @param token the raw binder used by OEM Extensions implementation.
    +         */
    +        @RequiresVendorApiLevel(level = 5)
    +        @NonNull
    +        public static Token createFromBinder(@NonNull IBinder token) {
    +            return new Token(token);
    +        }
    +
    +        /**
    +         * Retrieves an {@link ActivityStack} token from {@link Bundle} if it's valid.
    +         *
    +         * @param bundle the {@link Bundle} to retrieve the {@link ActivityStack} token from.
    +         * @throws IllegalArgumentException if the {@code bundle} isn't valid.
    +         */
    +        @RequiresVendorApiLevel(level = 5)
    +        @NonNull
    +        public static Token readFromBundle(@NonNull Bundle bundle) {
    +            final IBinder token = bundle.getBinder(KEY_ACTIVITY_STACK_RAW_TOKEN);
    +
    +            if (token == null) {
    +                throw new IllegalArgumentException("Invalid bundle to create ActivityStack Token");
    +            }
    +            return new Token(token);
    +        }
    +
    +        /**
    +         * Converts the token to {@link Bundle}.
    +         * 

    + * See {@link ActivityEmbeddingOptionsProperties#KEY_ACTIVITY_STACK_TOKEN} for sample usage. + */ + @RequiresVendorApiLevel(level = 5) + @NonNull + public Bundle toBundle() { + final Bundle bundle = new Bundle(); + bundle.putBinder(KEY_ACTIVITY_STACK_RAW_TOKEN, mToken); + return bundle; + } + + @NonNull + IBinder getRawToken() { + return mToken; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Token)) return false; + Token token = (Token) o; + return Objects.equals(mToken, token.mToken); + } + + @Override + public int hashCode() { + return Objects.hash(mToken); + } + + @NonNull + @Override + public String toString() { + return "Token{" + + "mToken=" + mToken + + '}'; + } + } }

    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java
    new file mode 100644
    index 0000000..eb4f743
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributes.java
    
    @@ -0,0 +1,143 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_ACTIVITY_STACK;
    +
    +import android.graphics.Rect;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +
    +/**
    + * Attributes used to update the layout and configuration of an {@link ActivityStack}.
    + */
    +public final class ActivityStackAttributes {
    +
    +    @NonNull
    +    private final Rect mRelativeBounds;
    +
    +    @NonNull
    +    private final WindowAttributes mWindowAttributes;
    +
    +    private ActivityStackAttributes(@NonNull Rect relativeBounds,
    +            @NonNull WindowAttributes windowAttributes) {
    +        mRelativeBounds = relativeBounds;
    +        mWindowAttributes = windowAttributes;
    +    }
    +
    +    /**
    +     * Returns the requested bounds of an {@link ActivityStack} which relative to its parent
    +     * container.
    +     * 

    + * {@link Rect#isEmpty() Empty} bounds mean that this {@link ActivityStack} should fill its + * parent container bounds. + */ + @RequiresVendorApiLevel(level = 6) + @NonNull + public Rect getRelativeBounds() { + return mRelativeBounds; + } + + /** + * Returns the {@link WindowAttributes} which contains the configurations of the embedded + * Activity windows with this attributes. + */ + @RequiresVendorApiLevel(level = 6) + @NonNull + public WindowAttributes getWindowAttributes() { + return mWindowAttributes; + } + + @Override + public int hashCode() { + return mRelativeBounds.hashCode() * 31 + mWindowAttributes.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + if (!(obj instanceof ActivityStackAttributes)) return false; + final ActivityStackAttributes attrs = (ActivityStackAttributes) obj; + return mRelativeBounds.equals(attrs.mRelativeBounds) + && mWindowAttributes.equals(attrs.mWindowAttributes); + } + + @NonNull + @Override + public String toString() { + return ActivityStackAttributes.class.getSimpleName() + ": {" + + " relativeBounds=" + mRelativeBounds + + ", windowAttributes=" + mWindowAttributes + + "}"; + } + + /** The builder class of {@link ActivityStackAttributes}. */ + public static final class Builder { + + /** The {@link ActivityStackAttributes} builder constructor. */ + @RequiresVendorApiLevel(level = 6) + public Builder() {} + + @NonNull + private final Rect mRelativeBounds = new Rect(); + + @NonNull + private WindowAttributes mWindowAttributes = + new WindowAttributes(DIM_AREA_ON_ACTIVITY_STACK); + + /** + * Sets the requested relative bounds of the {@link ActivityStack}. If this value is + * not specified, {@link #getRelativeBounds()} defaults to {@link Rect#isEmpty() empty} + * bounds, which means to follow the parent container bounds. + * + * @param relativeBounds The requested relative bounds. + * @return This {@code Builder}. + */ + @RequiresVendorApiLevel(level = 6) + @NonNull + public Builder setRelativeBounds(@NonNull Rect relativeBounds) { + mRelativeBounds.set(relativeBounds); + return this; + } + + /** + * Sets the window attributes. If this value is not specified, the + * {@link WindowAttributes#getDimAreaBehavior()} will be only applied on the + * {@link ActivityStack} of the requested activity. + * + * @param attributes The {@link WindowAttributes} + * @return This {@code Builder}. + */ + @NonNull + @RequiresVendorApiLevel(level = 6) + public Builder setWindowAttributes(@NonNull WindowAttributes attributes) { + mWindowAttributes = attributes; + return this; + } + + /** + * Builds an {@link ActivityStackAttributes} instance. + */ + @RequiresVendorApiLevel(level = 6) + @NonNull + public ActivityStackAttributes build() { + return new ActivityStackAttributes(mRelativeBounds, mWindowAttributes); + } + } +}

    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java
    new file mode 100644
    index 0000000..99afdc6
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStackAttributesCalculatorParams.java
    
    @@ -0,0 +1,107 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.os.Bundle;
    +
    +import androidx.annotation.NonNull;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +import androidx.window.extensions.core.util.function.Function;
    +
    +/**
    + * The parameter container used in standalone {@link ActivityStack} calculator function to report
    + * {@link ParentContainerInfo} and associated {@link ActivityStack#getTag()} to calculate
    + * {@link ActivityStackAttributes} when there's a parent container information update or a
    + * standalone {@link ActivityStack} is going to be launched.
    + *
    + * @see ActivityEmbeddingComponent#setActivityStackAttributesCalculator(Function)
    + */
    +public class ActivityStackAttributesCalculatorParams {
    +
    +    @NonNull
    +    private final ParentContainerInfo mParentContainerInfo;
    +
    +    @NonNull
    +    private final String mActivityStackTag;
    +
    +    @NonNull
    +    private final Bundle mLaunchOptions;
    +
    +    /**
    +     * {@code ActivityStackAttributesCalculatorParams} constructor.
    +     *
    +     * @param parentContainerInfo The {@link ParentContainerInfo} of the standalone
    +     *                            {@link ActivityStack} to apply the
    +     *                            {@link ActivityStackAttributes}.
    +     * @param activityStackTag The unique identifier of {@link ActivityStack} to apply the
    +     *                         {@link ActivityStackAttributes}.
    +     * @param launchOptions The options to launch the {@link ActivityStack}.
    +     */
    +    ActivityStackAttributesCalculatorParams(@NonNull ParentContainerInfo parentContainerInfo,
    +            @NonNull String activityStackTag, @NonNull Bundle launchOptions) {
    +        mParentContainerInfo = parentContainerInfo;
    +        mActivityStackTag = activityStackTag;
    +        mLaunchOptions = launchOptions;
    +    }
    +
    +    /**
    +     * Returns {@link ParentContainerInfo} of the standalone {@link ActivityStack} to calculate.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public ParentContainerInfo getParentContainerInfo() {
    +        return mParentContainerInfo;
    +    }
    +
    +    /**
    +     * Returns unique identifier of the standalone {@link ActivityStack} to calculate.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public String getActivityStackTag() {
    +        return mActivityStackTag;
    +    }
    +
    +    /**
    +     * Returns options that passed from WM Jetpack to WM Extensions library to launch an
    +     * {@link ActivityStack}. {@link Bundle#isEmpty() empty} options mean there's no launch options.
    +     * 

    + * For example, an {@link ActivityStack} launch options could be an + * {@link android.app.ActivityOptions} bundle that contains information to build an overlay + * {@link ActivityStack}. + *

    + * The launch options will be used for initializing standalone {@link ActivityStack} with + * {@link #getActivityStackTag()} specified. The logic is owned by WM Jetpack, which is usually + * from the {@link android.app.ActivityOptions}, WM Extensions library must not touch the + * options. + */ + @RequiresVendorApiLevel(level = 6) + @NonNull + public Bundle getLaunchOptions() { + return mLaunchOptions; + } + + @NonNull + @Override + public String toString() { + return ActivityStackAttributesCalculatorParams.class.getSimpleName() + ":{" + + "parentContainerInfo=" + mParentContainerInfo + + "activityStackTag=" + mActivityStackTag + + "launchOptions=" + mLaunchOptions + + "}"; + } +}

    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationBackground.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationBackground.java
    new file mode 100644
    index 0000000..0ca251e
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/AnimationBackground.java
    
    @@ -0,0 +1,117 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.graphics.Color;
    +
    +import androidx.annotation.ColorInt;
    +import androidx.annotation.NonNull;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +
    +import java.util.Objects;
    +
    +/**
    + * A class to represent the background to show while animating embedding activity containers if the
    + * animation requires a background.
    + *
    + * @see SplitAttributes.Builder#setAnimationBackground
    + */
    +public abstract class AnimationBackground {
    +
    +    /**
    +     * The special {@link AnimationBackground} object representing the default option.
    +     * When used, the system will determine the color to use, such as using the current theme
    +     * window background color.
    +     */
    +    @RequiresVendorApiLevel(level = 5)
    +    @NonNull
    +    public static final AnimationBackground ANIMATION_BACKGROUND_DEFAULT = new DefaultBackground();
    +
    +    /**
    +     * Creates a {@link ColorBackground} that wraps the given color.
    +     * Only opaque background is supported.
    +     *
    +     * @param color the color to be stored.
    +     * @throws IllegalArgumentException if the color is not opaque.
    +     */
    +    @RequiresVendorApiLevel(level = 5)
    +    @NonNull
    +    public static ColorBackground createColorBackground(@ColorInt int color) {
    +        return new ColorBackground(color);
    +    }
    +
    +    private AnimationBackground() {}
    +
    +    /** @see #ANIMATION_BACKGROUND_DEFAULT */
    +    private static class DefaultBackground extends AnimationBackground {
    +        @Override
    +        public String toString() {
    +            return DefaultBackground.class.getSimpleName();
    +        }
    +    }
    +
    +    /**
    +     * An {@link AnimationBackground} to specify of using a developer-defined color as the
    +     * animation background.
    +     * Only opaque background is supported.
    +     *
    +     * @see #createColorBackground(int)
    +     */
    +    @RequiresVendorApiLevel(level = 5)
    +    public static class ColorBackground extends AnimationBackground {
    +
    +        @ColorInt
    +        private final int mColor;
    +
    +        private ColorBackground(@ColorInt int color) {
    +            final int alpha = Color.alpha(color);
    +            if (alpha != 255) {
    +                throw new IllegalArgumentException(
    +                        "Color must be fully opaque, current alpha is " + alpha);
    +            }
    +            mColor = color;
    +        }
    +
    +        /**
    +         * Returns the color to use as the animation background.
    +         */
    +        @RequiresVendorApiLevel(level = 5)
    +        @ColorInt
    +        public int getColor() {
    +            return mColor;
    +        }
    +
    +        @Override
    +        public boolean equals(Object o) {
    +            if (this == o) return true;
    +            if (!(o instanceof ColorBackground)) return false;
    +            final ColorBackground that = (ColorBackground) o;
    +            return mColor == that.mColor;
    +        }
    +
    +        @Override
    +        public int hashCode() {
    +            return Objects.hash(mColor);
    +        }
    +
    +        @NonNull
    +        @Override
    +        public String toString() {
    +            return ColorBackground.class.getSimpleName() + " { color: " + mColor + " }";
    +        }
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java
    new file mode 100644
    index 0000000..f38daff
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/DividerAttributes.java
    
    @@ -0,0 +1,420 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.graphics.Color;
    +
    +import androidx.annotation.ColorInt;
    +import androidx.annotation.Dimension;
    +import androidx.annotation.IntDef;
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +import java.util.Objects;
    +
    +/**
    + * The attributes of the divider layout and behavior.
    + *
    + * @see SplitAttributes.Builder#setDividerAttributes(DividerAttributes)
    + */
    +public final class DividerAttributes {
    +
    +    /**
    +     * A divider type that draws a static line between the primary and secondary containers.
    +     */
    +    public static final int DIVIDER_TYPE_FIXED = 1;
    +
    +    /**
    +     * A divider type that draws a line between the primary and secondary containers with a drag
    +     * handle that the user can drag and resize the containers.
    +     */
    +    public static final int DIVIDER_TYPE_DRAGGABLE = 2;
    +
    +    @IntDef({DIVIDER_TYPE_FIXED, DIVIDER_TYPE_DRAGGABLE})
    +    @Retention(RetentionPolicy.SOURCE)
    +    @interface DividerType {
    +    }
    +
    +    /**
    +     * A special value to indicate that the ratio is unset. which means the system will choose a
    +     * default value based on the display size and form factor.
    +     *
    +     * @see #getPrimaryMinRatio()
    +     * @see #getPrimaryMaxRatio()
    +     */
    +    public static final float RATIO_SYSTEM_DEFAULT = -1.0f;
    +
    +    /**
    +     * A special value to indicate that the width is unset. which means the system will choose a
    +     * default value based on the display size and form factor.
    +     *
    +     * @see #getWidthDp()
    +     */
    +    public static final int WIDTH_SYSTEM_DEFAULT = -1;
    +
    +    /** The {@link DividerType}. */
    +    private final @DividerType int mDividerType;
    +
    +    /**
    +     * The divider width in dp. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the system
    +     * will choose a default value based on the display size and form factor.
    +     */
    +    private final @Dimension int mWidthDp;
    +
    +    /**
    +     * The min split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},
    +     * the system will choose a default value based on the display size and form factor. Will only
    +     * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
    +     * beyond this ratio, and when dragging is finished, the system will choose to either fully
    +     * expand the secondary container or move the divider back to this ratio.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
    +     * drag beyond this ratio.
    +     *
    +     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
    +     */
    +    private final float mPrimaryMinRatio;
    +
    +    /**
    +     * The max split ratio for the primary container. It defaults to {@link #RATIO_SYSTEM_DEFAULT},
    +     * the system will choose a default value based on the display size and form factor. Will only
    +     * be used when the divider type is {@link #DIVIDER_TYPE_DRAGGABLE}.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
    +     * beyond this ratio, and when dragging is finished, the system will choose to either fully
    +     * expand the primary container or move the divider back to this ratio.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
    +     * drag beyond this ratio.
    +     *
    +     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
    +     */
    +    private final float mPrimaryMaxRatio;
    +
    +    /** The color of the divider. */
    +    private final @ColorInt int mDividerColor;
    +
    +    /** Whether it is allowed to expand a container to full screen by dragging the divider. */
    +    private final boolean mIsDraggingToFullscreenAllowed;
    +
    +    /**
    +     * Constructor of {@link DividerAttributes}.
    +     *
    +     * @param dividerType                   the divider type. See {@link DividerType}.
    +     * @param widthDp                       the width of the divider.
    +     * @param primaryMinRatio               the min split ratio for the primary container.
    +     * @param primaryMaxRatio               the max split ratio for the primary container.
    +     * @param dividerColor                  the color of the divider.
    +     * @param isDraggingToFullscreenAllowed whether it is allowed to expand a container to full
    +     *                                      screen by dragging the divider.
    +     * @throws IllegalStateException if the provided values are invalid.
    +     */
    +    private DividerAttributes(
    +            @DividerType int dividerType,
    +            @Dimension int widthDp,
    +            float primaryMinRatio,
    +            float primaryMaxRatio,
    +            @ColorInt int dividerColor,
    +            boolean isDraggingToFullscreenAllowed) {
    +        if (dividerType == DIVIDER_TYPE_FIXED
    +                && (primaryMinRatio != RATIO_SYSTEM_DEFAULT
    +                || primaryMaxRatio != RATIO_SYSTEM_DEFAULT)) {
    +            throw new IllegalStateException(
    +                    "primaryMinRatio and primaryMaxRatio must be RATIO_SYSTEM_DEFAULT for "
    +                            + "DIVIDER_TYPE_FIXED.");
    +        }
    +        if (primaryMinRatio != RATIO_SYSTEM_DEFAULT && primaryMaxRatio != RATIO_SYSTEM_DEFAULT
    +                && primaryMinRatio > primaryMaxRatio) {
    +            throw new IllegalStateException(
    +                    "primaryMinRatio must be less than or equal to primaryMaxRatio");
    +        }
    +        mDividerType = dividerType;
    +        mWidthDp = widthDp;
    +        mPrimaryMinRatio = primaryMinRatio;
    +        mPrimaryMaxRatio = primaryMaxRatio;
    +        mDividerColor = dividerColor;
    +        mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;
    +    }
    +
    +    /**
    +     * Returns the divider type.
    +     *
    +     * @see #DIVIDER_TYPE_FIXED
    +     * @see #DIVIDER_TYPE_DRAGGABLE
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public @DividerType int getDividerType() {
    +        return mDividerType;
    +    }
    +
    +    /**
    +     * Returns the width of the divider. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means
    +     * the system will choose a default value based on the display size and form factor.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public @Dimension int getWidthDp() {
    +        return mWidthDp;
    +    }
    +
    +    /**
    +     * Returns the min split ratio for the primary container the divider can be dragged to. It
    +     * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
    +     * based on the display size and form factor. Will only be used when the divider type is
    +     * {@link #DIVIDER_TYPE_DRAGGABLE}.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
    +     * beyond this ratio, and when dragging is finished, the system will choose to either fully
    +     * expand the secondary container or move the divider back to this ratio.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
    +     * drag beyond this ratio.
    +     *
    +     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public float getPrimaryMinRatio() {
    +        return mPrimaryMinRatio;
    +    }
    +
    +    /**
    +     * Returns the max split ratio for the primary container the divider can be dragged to. It
    +     * defaults to {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
    +     * based on the display size and form factor. Will only be used when the divider type is
    +     * {@link #DIVIDER_TYPE_DRAGGABLE}.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
    +     * beyond this ratio, and when dragging is finished, the system will choose to either fully
    +     * expand the primary container or move the divider back to this ratio.
    +     *
    +     * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
    +     * drag beyond this ratio.
    +     *
    +     * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public float getPrimaryMaxRatio() {
    +        return mPrimaryMaxRatio;
    +    }
    +
    +    /** Returns the color of the divider. */
    +    @RequiresVendorApiLevel(level = 6)
    +    public @ColorInt int getDividerColor() {
    +        return mDividerColor;
    +    }
    +
    +    /**
    +     * Returns whether it is allowed to expand a container to full screen by dragging the
    +     * divider. Default is {@code true}.
    +     */
    +    @RequiresVendorApiLevel(level = 7)
    +    public boolean isDraggingToFullscreenAllowed() {
    +        return mIsDraggingToFullscreenAllowed;
    +    }
    +
    +    @Override
    +    public boolean equals(@Nullable Object obj) {
    +        if (this == obj) return true;
    +        if (!(obj instanceof DividerAttributes)) return false;
    +        final DividerAttributes other = (DividerAttributes) obj;
    +        return mDividerType == other.mDividerType
    +                && mWidthDp == other.mWidthDp
    +                && mPrimaryMinRatio == other.mPrimaryMinRatio
    +                && mPrimaryMaxRatio == other.mPrimaryMaxRatio
    +                && mDividerColor == other.mDividerColor
    +                && mIsDraggingToFullscreenAllowed == other.mIsDraggingToFullscreenAllowed;
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        return Objects.hash(mDividerType, mWidthDp, mPrimaryMinRatio, mPrimaryMaxRatio,
    +                mIsDraggingToFullscreenAllowed);
    +    }
    +
    +    @NonNull
    +    @Override
    +    public String toString() {
    +        return DividerAttributes.class.getSimpleName() + "{"
    +                + "dividerType=" + mDividerType
    +                + ", width=" + mWidthDp
    +                + ", minPrimaryRatio=" + mPrimaryMinRatio
    +                + ", maxPrimaryRatio=" + mPrimaryMaxRatio
    +                + ", dividerColor=" + mDividerColor
    +                + ", isDraggingToFullscreenAllowed=" + mIsDraggingToFullscreenAllowed
    +                + "}";
    +    }
    +
    +    /** The {@link DividerAttributes} builder. */
    +    public static final class Builder {
    +
    +        private final @DividerType int mDividerType;
    +
    +        private @Dimension int mWidthDp = WIDTH_SYSTEM_DEFAULT;
    +
    +        private float mPrimaryMinRatio = RATIO_SYSTEM_DEFAULT;
    +
    +        private float mPrimaryMaxRatio = RATIO_SYSTEM_DEFAULT;
    +
    +        private @ColorInt int mDividerColor = Color.BLACK;
    +
    +        private boolean mIsDraggingToFullscreenAllowed = false;
    +
    +        /**
    +         * The {@link DividerAttributes} builder constructor.
    +         *
    +         * @param dividerType the divider type, possible values are {@link #DIVIDER_TYPE_FIXED} and
    +         *                    {@link #DIVIDER_TYPE_DRAGGABLE}.
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder(@DividerType int dividerType) {
    +            mDividerType = dividerType;
    +        }
    +
    +        /**
    +         * The {@link DividerAttributes} builder constructor initialized by an existing
    +         * {@link DividerAttributes}.
    +         *
    +         * @param original the original {@link DividerAttributes} to initialize the {@link Builder}.
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder(@NonNull DividerAttributes original) {
    +            Objects.requireNonNull(original);
    +            mDividerType = original.mDividerType;
    +            mWidthDp = original.mWidthDp;
    +            mPrimaryMinRatio = original.mPrimaryMinRatio;
    +            mPrimaryMaxRatio = original.mPrimaryMaxRatio;
    +            mDividerColor = original.mDividerColor;
    +            mIsDraggingToFullscreenAllowed = original.mIsDraggingToFullscreenAllowed;
    +        }
    +
    +        /**
    +         * Sets the divider width. It defaults to {@link #WIDTH_SYSTEM_DEFAULT}, which means the
    +         * system will choose a default value based on the display size and form factor.
    +         *
    +         * @throws IllegalArgumentException if the provided value is invalid.
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        @NonNull
    +        public Builder setWidthDp(@Dimension int widthDp) {
    +            if (widthDp != WIDTH_SYSTEM_DEFAULT && widthDp < 0) {
    +                throw new IllegalArgumentException(
    +                        "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT.");
    +            }
    +            mWidthDp = widthDp;
    +            return this;
    +        }
    +
    +        /**
    +         * Sets the min split ratio for the primary container. It defaults to
    +         * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value based
    +         * on the display size and form factor. Will only be used when the divider type is
    +         * {@link #DIVIDER_TYPE_DRAGGABLE}.
    +         *
    +         * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
    +         * beyond this ratio, and when dragging is finished, the system will choose to either fully
    +         * expand the secondary container or move the divider back to this ratio.
    +         *
    +         * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
    +         * drag beyond this ratio.
    +         *
    +         * @param primaryMinRatio the min ratio for the primary container. Must be in range
    +         *                        [0.0, 1.0) or {@link #RATIO_SYSTEM_DEFAULT}.
    +         * @throws IllegalArgumentException if the provided value is invalid.
    +         * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        @NonNull
    +        public Builder setPrimaryMinRatio(float primaryMinRatio) {
    +            if (primaryMinRatio != RATIO_SYSTEM_DEFAULT
    +                    && (primaryMinRatio >= 1.0 || primaryMinRatio < 0.0)) {
    +                throw new IllegalArgumentException(
    +                        "primaryMinRatio must be in [0.0, 1.0) or RATIO_SYSTEM_DEFAULT.");
    +            }
    +            mPrimaryMinRatio = primaryMinRatio;
    +            return this;
    +        }
    +
    +        /**
    +         * Sets the max split ratio for the primary container. It defaults to
    +         * {@link #RATIO_SYSTEM_DEFAULT}, which means the system will choose a default value
    +         * based on the display size and form factor. Will only be used when the divider type is
    +         * {@link #DIVIDER_TYPE_DRAGGABLE}.
    +         *
    +         * If {@link #isDraggingToFullscreenAllowed()} is {@code true}, the user is allowed to drag
    +         * beyond this ratio, and when dragging is finished, the system will choose to either fully
    +         * expand the primary container or move the divider back to this ratio.
    +         *
    +         * If {@link #isDraggingToFullscreenAllowed()} is {@code false}, the user is not allowed to
    +         * drag beyond this ratio.
    +         *
    +         * @param primaryMaxRatio the max ratio for the primary container. Must be in range
    +         *                        (0.0, 1.0] or {@link #RATIO_SYSTEM_DEFAULT}.
    +         * @throws IllegalArgumentException if the provided value is invalid.
    +         * @see SplitAttributes.SplitType.RatioSplitType#getRatio()
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        @NonNull
    +        public Builder setPrimaryMaxRatio(float primaryMaxRatio) {
    +            if (primaryMaxRatio != RATIO_SYSTEM_DEFAULT
    +                    && (primaryMaxRatio > 1.0 || primaryMaxRatio <= 0.0)) {
    +                throw new IllegalArgumentException(
    +                        "primaryMaxRatio must be in (0.0, 1.0] or RATIO_SYSTEM_DEFAULT.");
    +            }
    +            mPrimaryMaxRatio = primaryMaxRatio;
    +            return this;
    +        }
    +
    +        /**
    +         * Sets the color of the divider. If not set, the default color {@link Color#BLACK} is
    +         * used.
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        @NonNull
    +        public Builder setDividerColor(@ColorInt int dividerColor) {
    +            mDividerColor = dividerColor;
    +            return this;
    +        }
    +
    +        /**
    +         * Sets whether it is allowed to expand a container to full screen by dragging the divider.
    +         * Default is {@code true}.
    +         */
    +        @RequiresVendorApiLevel(level = 7)
    +        @NonNull
    +        public Builder setDraggingToFullscreenAllowed(boolean isDraggingToFullscreenAllowed) {
    +            mIsDraggingToFullscreenAllowed = isDraggingToFullscreenAllowed;
    +            return this;
    +        }
    +
    +        /**
    +         * Builds a {@link DividerAttributes} instance.
    +         *
    +         * @return a {@link DividerAttributes} instance.
    +         * @throws IllegalArgumentException if the provided values are invalid.
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        @NonNull
    +        public DividerAttributes build() {
    +            return new DividerAttributes(mDividerType, mWidthDp, mPrimaryMinRatio,
    +                    mPrimaryMaxRatio, mDividerColor, mIsDraggingToFullscreenAllowed);
    +        }
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java
    new file mode 100644
    index 0000000..afea328
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddedActivityWindowInfo.java
    
    @@ -0,0 +1,119 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.app.Activity;
    +import android.graphics.Rect;
    +
    +import androidx.annotation.NonNull;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +
    +import java.util.Objects;
    +
    +/**
    + * Describes the embedded window related info of an activity.
    + *
    + * @see ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback
    + * @see ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo
    + */
    +public class EmbeddedActivityWindowInfo {
    +
    +    @NonNull
    +    private final Activity mActivity;
    +    private final boolean mIsEmbedded;
    +    @NonNull
    +    private final Rect mTaskBounds;
    +    @NonNull
    +    private final Rect mActivityStackBounds;
    +
    +    EmbeddedActivityWindowInfo(@NonNull Activity activity, boolean isEmbedded,
    +            @NonNull Rect taskBounds, @NonNull Rect activityStackBounds) {
    +        mActivity = Objects.requireNonNull(activity);
    +        mIsEmbedded = isEmbedded;
    +        mTaskBounds = Objects.requireNonNull(taskBounds);
    +        mActivityStackBounds = Objects.requireNonNull(activityStackBounds);
    +    }
    +
    +    /**
    +     * Returns the {@link Activity} this {@link EmbeddedActivityWindowInfo} is about.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public Activity getActivity() {
    +        return mActivity;
    +    }
    +
    +    /**
    +     * Whether this activity is embedded, which means it is in an ActivityStack window that
    +     * doesn't fill the Task.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public boolean isEmbedded() {
    +        return mIsEmbedded;
    +    }
    +
    +    /**
    +     * Returns the bounds of the Task window in display space.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public Rect getTaskBounds() {
    +        return mTaskBounds;
    +    }
    +
    +    /**
    +     * Returns the bounds of the ActivityStack window in display space.
    +     * This can be referring to the bounds of the same window as {@link #getTaskBounds()} when
    +     * the activity is not embedded.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public Rect getActivityStackBounds() {
    +        return mActivityStackBounds;
    +    }
    +
    +    @Override
    +    public boolean equals(Object o) {
    +        if (this == o) return true;
    +        if (!(o instanceof EmbeddedActivityWindowInfo)) return false;
    +        final EmbeddedActivityWindowInfo that = (EmbeddedActivityWindowInfo) o;
    +        return mActivity.equals(that.mActivity)
    +                && mIsEmbedded == that.mIsEmbedded
    +                && mTaskBounds.equals(that.mTaskBounds)
    +                && mActivityStackBounds.equals(that.mActivityStackBounds);
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        int result = mActivity.hashCode();
    +        result = result * 31 + (mIsEmbedded ? 1 : 0);
    +        result = result * 31 + mTaskBounds.hashCode();
    +        result = result * 31 + mActivityStackBounds.hashCode();
    +        return result;
    +    }
    +
    +    @NonNull
    +    @Override
    +    public String toString() {
    +        return "EmbeddedActivityWindowInfo{"
    +                + "activity=" + mActivity
    +                + ", isEmbedded=" + mIsEmbedded
    +                + ", taskBounds=" + mTaskBounds
    +                + ", activityStackBounds=" + mActivityStackBounds
    +                + "}";
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java
    new file mode 100644
    index 0000000..e08b803
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ParentContainerInfo.java
    
    @@ -0,0 +1,106 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.content.res.Configuration;
    +import android.view.WindowMetrics;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +import androidx.window.extensions.layout.WindowLayoutInfo;
    +
    +/**
    + * The parent container information of an {@link ActivityStack}.
    + * The data class is designed to provide information to calculate the presentation of
    + * an {@link ActivityStack}.
    + */
    +@RequiresVendorApiLevel(level = 6)
    +public class ParentContainerInfo {
    +    @NonNull
    +    private final WindowMetrics mWindowMetrics;
    +
    +    @NonNull
    +    private final Configuration mConfiguration;
    +
    +    @NonNull
    +    private final WindowLayoutInfo mWindowLayoutInfo;
    +
    +    /**
    +     * {@link ParentContainerInfo} constructor, which is used in Window Manager Extensions to
    +     * provide information of a parent window container.
    +     *
    +     * @param windowMetrics The parent container's {@link WindowMetrics}
    +     * @param configuration The parent container's {@link Configuration}
    +     * @param windowLayoutInfo The parent container's {@link WindowLayoutInfo}
    +     */
    +    ParentContainerInfo(@NonNull WindowMetrics windowMetrics, @NonNull Configuration configuration,
    +            @NonNull WindowLayoutInfo windowLayoutInfo) {
    +        mWindowMetrics = windowMetrics;
    +        mConfiguration = configuration;
    +        mWindowLayoutInfo = windowLayoutInfo;
    +    }
    +
    +    /** Returns the parent container's {@link WindowMetrics}. */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public WindowMetrics getWindowMetrics() {
    +        return mWindowMetrics;
    +    }
    +
    +    /** Returns the parent container's {@link Configuration}. */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public Configuration getConfiguration() {
    +        return mConfiguration;
    +    }
    +
    +    /** Returns the parent container's {@link WindowLayoutInfo}. */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    public WindowLayoutInfo getWindowLayoutInfo() {
    +        return mWindowLayoutInfo;
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        int result = mWindowMetrics.hashCode();
    +        result = 31 * result + mConfiguration.hashCode();
    +        result = 31 * result + mWindowLayoutInfo.hashCode();
    +        return result;
    +    }
    +
    +    @Override
    +    public boolean equals(@Nullable Object obj) {
    +        if (this == obj) return true;
    +        if (!(obj instanceof ParentContainerInfo)) return false;
    +        final ParentContainerInfo parentContainerInfo = (ParentContainerInfo) obj;
    +        return mWindowMetrics.equals(parentContainerInfo.mWindowMetrics)
    +                && mConfiguration.equals(parentContainerInfo.mConfiguration)
    +                && mWindowLayoutInfo.equals(parentContainerInfo.mWindowLayoutInfo);
    +    }
    +
    +    @NonNull
    +    @Override
    +    public String toString() {
    +        return ParentContainerInfo.class.getSimpleName() + ": {"
    +                + "windowMetrics=" + WindowMetricsCompat.toString(mWindowMetrics)
    +                + ", configuration=" + mConfiguration
    +                + ", windowLayoutInfo=" + mWindowLayoutInfo
    +                + "}";
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
    index c5c4c78..88e53c4 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
    
    @@ -16,27 +16,25 @@
     
     package androidx.window.extensions.embedding;
     
    -import static androidx.annotation.RestrictTo.Scope.LIBRARY;
     import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.BOTTOM_TO_TOP;
     import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.LEFT_TO_RIGHT;
     import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.LOCALE;
     import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.RIGHT_TO_LEFT;
     import static androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.TOP_TO_BOTTOM;
    +import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
     
     import android.annotation.SuppressLint;
    -import android.graphics.Color;
     
    -import androidx.annotation.ColorInt;
     import androidx.annotation.FloatRange;
     import androidx.annotation.IntDef;
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    -import androidx.annotation.RestrictTo;
     import androidx.window.extensions.RequiresVendorApiLevel;
     import androidx.window.extensions.core.util.function.Function;
     
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
    +import java.util.Objects;
     
     /**
      * Attributes that describe how the parent window (typically the activity task
    @@ -50,9 +48,9 @@
      *         vertically or horizontally and in which direction the primary and
      *         secondary containers are respectively positioned (left to right,
      *         right to left, top to bottom, and so forth)
  • - *
  • Animation background color -- The color of the background during - * animation of the split involving this {@code SplitAttributes} object - * if the animation requires a background
  • + *
  • Animation background -- The background to show during animation of + * the split involving this {@code SplitAttributes} object if the + * animation requires a background
  • * * *

    Attributes can be configured by: @@ -67,22 +65,10 @@ * * @see SplitAttributes.SplitType * @see SplitAttributes.LayoutDirection + * @see AnimationBackground */ @RequiresVendorApiLevel(level = 2) -public class SplitAttributes { - - /** - * The default value for animation background color, which means to use the current theme window - * background color. - * - * Only opaque color is supported, so {@code 0} is used as the default. Any other non-opaque - * color will be treated as the default. - * - * @see Builder#setAnimationBackgroundColor(int) - */ - @ColorInt - @RestrictTo(LIBRARY) - public static final int DEFAULT_ANIMATION_BACKGROUND_COLOR = 0; +public final class SplitAttributes { /** * The type of window split, which defines the proportion of the parent @@ -132,8 +118,9 @@ } /** - * A window split that's based on the ratio of the size of the primary - * container to the size of the parent window. + * A window split that's based on the ratio of the size of the primary container to the + * size of the parent window (excluding area unavailable for the containers such as the + * divider. See {@link DividerAttributes}). * *

    Values in the non-inclusive range (0.0, 1.0) define the size of * the primary container relative to the size of the parent window: @@ -155,11 +142,12 @@ /** * Creates an instance of this {@code RatioSplitType}. * - * @param ratio The proportion of the parent window occupied by the - * primary container of the split. Can be a value in the - * non-inclusive range (0.0, 1.0). Use - * {@link SplitType.ExpandContainersSplitType} to create a split - * type that occupies the entire parent window. + * @param ratio The proportion of the parent window occupied by the primary container + * of the split (excluding area unavailable for the containers such as + * the divider. See {@link DividerAttributes}). Can be a value in the + * non-inclusive range (0.0, 1.0). Use + * {@link SplitType.ExpandContainersSplitType} to create a split + * type that occupies the entire parent window. */ public RatioSplitType( @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) @@ -173,11 +161,12 @@ } /** - * Gets the proportion of the parent window occupied by the primary - * activity container of the split. + * Gets the proportion of the parent window occupied by the primary activity + * container of the split (excluding area unavailable for the containers such as the + * divider. See {@link DividerAttributes}) . * * @return The proportion of the split occupied by the primary - * container. + * container. */ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) public float getRatio() { @@ -193,7 +182,7 @@ * specified. * * @return A {@code RatioSplitType} in which the activity containers - * occupy equal portions of the parent window. + * occupy equal portions of the parent window. */ @NonNull public static RatioSplitType splitEqually() { @@ -235,9 +224,9 @@ * Creates an instance of this {@code HingeSplitType}. * * @param fallbackSplitType The split type to use if a split based - * on the device hinge or separating fold cannot be determined. - * Can be a {@link RatioSplitType} or - * {@link ExpandContainersSplitType}. + * on the device hinge or separating fold cannot be determined. + * Can be a {@link RatioSplitType} or + * {@link ExpandContainersSplitType}. */ public HingeSplitType(@NonNull SplitType fallbackSplitType) { super("hinge, fallbackType=" + fallbackSplitType); @@ -284,15 +273,15 @@ * * A possible return value of {@link SplitType#getLayoutDirection()}. */ - // - // ------------------------- - // | | | - // | Primary | Secondary | - // | | | - // ------------------------- - // - // Must match {@link LayoutDirection#LTR} for backwards compatibility - // with prior versions of Extensions. + // + // ------------------------- + // | | | + // | Primary | Secondary | + // | | | + // ------------------------- + // + // Must match {@link LayoutDirection#LTR} for backwards compatibility + // with prior versions of Extensions. public static final int LEFT_TO_RIGHT = 0; /** @@ -304,14 +293,14 @@ * * A possible return value of {@link SplitType#getLayoutDirection()}. */ - // ------------------------- - // | | | - // | Secondary | Primary | - // | | | - // ------------------------- - // - // Must match {@link LayoutDirection#RTL} for backwards compatibility - // with prior versions of Extensions. + // ------------------------- + // | | | + // | Secondary | Primary | + // | | | + // ------------------------- + // + // Must match {@link LayoutDirection#RTL} for backwards compatibility + // with prior versions of Extensions. public static final int RIGHT_TO_LEFT = 1; /** @@ -323,8 +312,8 @@ * * A possible return value of {@link SplitType#getLayoutDirection()}. */ - // Must match {@link LayoutDirection#LOCALE} for backwards - // compatibility with prior versions of Extensions. + // Must match {@link LayoutDirection#LOCALE} for backwards + // compatibility with prior versions of Extensions. public static final int LOCALE = 3; /** @@ -339,15 +328,15 @@ * * A possible return value of {@link SplitType#getLayoutDirection()}. */ - // ------------- - // | | - // | Primary | - // | | - // ------------- - // | | - // | Secondary | - // | | - // ------------- + // ------------- + // | | + // | Primary | + // | | + // ------------- + // | | + // | Secondary | + // | | + // ------------- public static final int TOP_TO_BOTTOM = 4; /** @@ -362,49 +351,70 @@ * * A possible return value of {@link SplitType#getLayoutDirection()}. */ - // ------------- - // | | - // | Secondary | - // | | - // ------------- - // | | - // | Primary | - // | | - // ------------- + // ------------- + // | | + // | Secondary | + // | | + // ------------- + // | | + // | Primary | + // | | + // ------------- public static final int BOTTOM_TO_TOP = 5; - private LayoutDirection() {} + private LayoutDirection() { + } } @IntDef({LEFT_TO_RIGHT, RIGHT_TO_LEFT, LOCALE, TOP_TO_BOTTOM, BOTTOM_TO_TOP}) @Retention(RetentionPolicy.SOURCE) - @interface ExtLayoutDirection {} + @interface ExtLayoutDirection { + } @ExtLayoutDirection private final int mLayoutDirection; + @NonNull private final SplitType mSplitType; - @ColorInt - private final int mAnimationBackgroundColor; + @NonNull + private final AnimationBackground mAnimationBackground; + + @NonNull + private final WindowAttributes mWindowAttributes; + + /** The attributes of a divider. If {@code null}, no divider is requested. */ + @Nullable + private final DividerAttributes mDividerAttributes; /** * Creates an instance of this {@code SplitAttributes}. * - * @param splitType The type of split. See - * {@link SplitAttributes.SplitType}. - * @param layoutDirection The layout direction of the split, such as left to - * right or top to bottom. See {@link SplitAttributes.LayoutDirection}. - * @param animationBackgroundColor The {@link ColorInt} to use for the - * background color during animation of the split involving this - * {@code SplitAttributes} object if the animation requires a - * background. + * @param splitType The type of split. See + * {@link SplitAttributes.SplitType}. + * @param layoutDirection The layout direction of the split, such as left to + * right or top to bottom. See + * {@link SplitAttributes.LayoutDirection}. + * @param animationBackground The {@link AnimationBackground} to use for the during animation + * of the split involving this {@code SplitAttributes} object if the + * animation requires a background. + * @param attributes The {@link WindowAttributes} of the split, such as dim area + * behavior. + * @param dividerAttributes The {@link DividerAttributes}. If {@code null}, no divider is + * requested. */ - SplitAttributes(@NonNull SplitType splitType, @ExtLayoutDirection int layoutDirection, - @ColorInt int animationBackgroundColor) { + SplitAttributes( + @NonNull SplitType splitType, + @ExtLayoutDirection int layoutDirection, + @NonNull AnimationBackground animationBackground, + @NonNull WindowAttributes attributes, + @Nullable DividerAttributes dividerAttributes + ) { mSplitType = splitType; mLayoutDirection = layoutDirection; - mAnimationBackgroundColor = animationBackgroundColor; + mAnimationBackground = animationBackground; + mWindowAttributes = attributes; + mDividerAttributes = dividerAttributes; } /** @@ -428,18 +438,30 @@ } /** - * Gets the {@link ColorInt} to use for the background color during the + * Returns the {@link AnimationBackground} to use for the background during the * animation of the split involving this {@code SplitAttributes} object. - * - * The default is {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}, which means - * to use the current theme window background color. - * - * @return The animation background {@code ColorInt}. */ - @ColorInt - @RestrictTo(LIBRARY) - public int getAnimationBackgroundColor() { - return mAnimationBackgroundColor; + @NonNull + @RequiresVendorApiLevel(level = 5) + public AnimationBackground getAnimationBackground() { + return mAnimationBackground; + } + + /** + * Returns the {@link WindowAttributes} which contains the configurations of the embedded + * Activity windows in this SplitAttributes. + */ + @NonNull + @RequiresVendorApiLevel(level = 5) + public WindowAttributes getWindowAttributes() { + return mWindowAttributes; + } + + /** Returns the {@link DividerAttributes}. If {@code null}, no divider is requested. */ + @RequiresVendorApiLevel(level = 6) + @Nullable + public DividerAttributes getDividerAttributes() { + return mDividerAttributes; } /** @@ -447,16 +469,42 @@ * * - The default split type is an equal split between primary and secondary containers. * - The default layout direction is based on locale. - * - The default animation background color is to use the current theme window background color. + * - The default animation background is to use the current theme window background color. */ public static final class Builder { @NonNull - private SplitType mSplitType = new SplitType.RatioSplitType(0.5f); + private SplitType mSplitType = new SplitType.RatioSplitType(0.5f); @ExtLayoutDirection private int mLayoutDirection = LOCALE; - @ColorInt - private int mAnimationBackgroundColor = 0; + @NonNull + private AnimationBackground mAnimationBackground = + AnimationBackground.ANIMATION_BACKGROUND_DEFAULT; + + @NonNull + private WindowAttributes mWindowAttributes = + new WindowAttributes(DIM_AREA_ON_TASK); + + @Nullable + private DividerAttributes mDividerAttributes; + + /** Creates a new {@link Builder} to create {@link SplitAttributes}. */ + public Builder() { + } + + /** + * Creates a {@link Builder} with values cloned from the original {@link SplitAttributes}. + * + * @param original the original {@link SplitAttributes} to initialize the {@link Builder}. + */ + @RequiresVendorApiLevel(level = 6) + public Builder(@NonNull SplitAttributes original) { + mSplitType = original.mSplitType; + mLayoutDirection = original.mLayoutDirection; + mAnimationBackground = original.mAnimationBackground; + mWindowAttributes = original.mWindowAttributes; + mDividerAttributes = original.mDividerAttributes; + } /** * Sets the split type attribute. @@ -498,64 +546,76 @@ } /** - * Sets the {@link ColorInt} to use for the background during the + * Sets the {@link AnimationBackground} to use for the background during the * animation of the split involving this {@code SplitAttributes} object * if the animation requires a background. * - * Only opaque color is supported. + * The default value is {@link AnimationBackground#ANIMATION_BACKGROUND_DEFAULT}, which + * means to use the current theme window background color. * - * The default value is {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}, which - * means to use the current theme window background color. Any non-opaque - * animation color will be treated as - * {@link #DEFAULT_ANIMATION_BACKGROUND_COLOR}. - * - * @param color A packed color int of the form {@code AARRGGBB} for the - * animation background color. + * @param background An {@link AnimationBackground} to be used for the animation of the + * split. * @return This {@code Builder}. */ @NonNull - @RestrictTo(LIBRARY) - public Builder setAnimationBackgroundColor(@ColorInt int color) { - // Any non-opaque color will be treated as the default. - mAnimationBackgroundColor = Color.alpha(color) != 255 - ? DEFAULT_ANIMATION_BACKGROUND_COLOR - : color; + @RequiresVendorApiLevel(level = 5) + public Builder setAnimationBackground(@NonNull AnimationBackground background) { + mAnimationBackground = background; + return this; + } + + /** + * Sets the window attributes. If this value is not specified, the + * {@link WindowAttributes#getDimAreaBehavior()} will be only applied on the + * {@link ActivityStack} of the requested activity. + * + * @param attributes The {@link WindowAttributes} + * @return This {@code Builder}. + */ + @NonNull + @RequiresVendorApiLevel(level = 5) + public Builder setWindowAttributes(@NonNull WindowAttributes attributes) { + mWindowAttributes = attributes; + return this; + } + + /** Sets the {@link DividerAttributes}. If {@code null}, no divider is requested. */ + @RequiresVendorApiLevel(level = 6) + @NonNull + public Builder setDividerAttributes(@Nullable DividerAttributes dividerAttributes) { + mDividerAttributes = dividerAttributes; return this; } /** * Builds a {@link SplitAttributes} instance with the attributes * specified by {@link #setSplitType}, {@link #setLayoutDirection}, and - * {@link #setAnimationBackgroundColor}. + * {@link #setAnimationBackground}. * * @return The new {@code SplitAttributes} instance. */ @NonNull public SplitAttributes build() { - return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackgroundColor); + return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackground, + mWindowAttributes, mDividerAttributes); } } @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SplitAttributes)) return false; + SplitAttributes that = (SplitAttributes) o; + return mLayoutDirection == that.mLayoutDirection && mSplitType.equals(that.mSplitType) + && mAnimationBackground.equals(that.mAnimationBackground) + && mWindowAttributes.equals(that.mWindowAttributes) + && Objects.equals(mDividerAttributes, that.mDividerAttributes); + } + + @Override public int hashCode() { - int result = mSplitType.hashCode(); - result = result * 31 + mLayoutDirection; - result = result * 31 + mAnimationBackgroundColor; - return result; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - if (!(other instanceof SplitAttributes)) { - return false; - } - final SplitAttributes otherAttributes = (SplitAttributes) other; - return mLayoutDirection == otherAttributes.mLayoutDirection - && mSplitType.equals(otherAttributes.mSplitType) - && mAnimationBackgroundColor == otherAttributes.mAnimationBackgroundColor; + return Objects.hash(mLayoutDirection, mSplitType, mAnimationBackground, mWindowAttributes, + mDividerAttributes); } @NonNull @@ -563,14 +623,16 @@ public String toString() { return SplitAttributes.class.getSimpleName() + "{" + "layoutDir=" + layoutDirectionToString() - + ", ratio=" + mSplitType - + ", animationBgColor=" + Integer.toHexString(mAnimationBackgroundColor) + + ", splitType=" + mSplitType + + ", animationBackground=" + mAnimationBackground + + ", windowAttributes=" + mWindowAttributes + + ", dividerAttributes=" + mDividerAttributes + "}"; } @NonNull private String layoutDirectionToString() { - switch(mLayoutDirection) { + switch (mLayoutDirection) { case LEFT_TO_RIGHT: return "LEFT_TO_RIGHT"; case RIGHT_TO_LEFT:

    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
    index 80f98fe..723a96f 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculatorParams.java
    
    @@ -17,12 +17,11 @@
     package androidx.window.extensions.embedding;
     
     import android.content.res.Configuration;
    -import android.os.Build;
     import android.view.WindowMetrics;
     
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
    -import androidx.annotation.RequiresApi;
    +import androidx.window.extensions.RequiresVendorApiLevel;
     import androidx.window.extensions.layout.WindowLayoutInfo;
     
     /**
    @@ -32,8 +31,8 @@
      * {@link SplitRule} by {@link #getSplitRuleTag()} if {@link SplitRule#getTag()} is specified.
      *
      * @see ActivityEmbeddingComponent#clearSplitAttributesCalculator()
    - * Since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
      */
    +@RequiresVendorApiLevel(level = 2)
     public class SplitAttributesCalculatorParams {
         @NonNull
         private final WindowMetrics mParentWindowMetrics;
    @@ -121,29 +120,11 @@
         @Override
         public String toString() {
             return getClass().getSimpleName() + ":{"
    -                + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
    +                + "windowMetrics=" + WindowMetricsCompat.toString(mParentWindowMetrics)
                     + ", configuration=" + mParentConfiguration
                     + ", windowLayoutInfo=" + mParentWindowLayoutInfo
                     + ", defaultSplitAttributes=" + mDefaultSplitAttributes
                     + ", areDefaultConstraintsSatisfied=" + mAreDefaultConstraintsSatisfied
                     + ", tag=" + mSplitRuleTag + "}";
         }
    -
    -    private static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
    -        // TODO(b/187712731): Use WindowMetrics#toString after it's implemented in U.
    -        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    -            return Api30Impl.windowMetricsToString(windowMetrics);
    -        }
    -        throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
    -    }
    -
    -    @RequiresApi(30)
    -    private static final class Api30Impl {
    -        static String windowMetricsToString(@NonNull WindowMetrics windowMetrics) {
    -            return WindowMetrics.class.getSimpleName() + ":{"
    -                    + "bounds=" + windowMetrics.getBounds()
    -                    + ", windowInsets=" + windowMetrics.getWindowInsets()
    -                    + "}";
    -        }
    -    }
     }
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
    index 7b79b1c..b8d9c66 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
    
    @@ -35,7 +35,7 @@
         private final SplitAttributes mSplitAttributes;
     
         @NonNull
    -    private final IBinder mToken;
    +    private final Token mToken;
     
         /**
          * The {@code SplitInfo} constructor
    @@ -48,7 +48,7 @@
         SplitInfo(@NonNull ActivityStack primaryActivityStack,
                 @NonNull ActivityStack secondaryActivityStack,
                 @NonNull SplitAttributes splitAttributes,
    -            @NonNull IBinder token) {
    +            @NonNull Token token) {
             Objects.requireNonNull(primaryActivityStack);
             Objects.requireNonNull(secondaryActivityStack);
             Objects.requireNonNull(splitAttributes);
    @@ -91,10 +91,18 @@
             return mSplitAttributes;
         }
     
    -    /** Returns a token uniquely identifying the container. */
    -    @RequiresVendorApiLevel(level = 3)
    +    /** @deprecated Use {@link #getSplitInfoToken()} instead. */
    +    @Deprecated
    +    @RequiresVendorApiLevel(level = 3, deprecatedSince = 5)
         @NonNull
         public IBinder getToken() {
    +        return mToken.getRawToken();
    +    }
    +
    +    /** Returns a token uniquely identifying the split. */
    +    @RequiresVendorApiLevel(level = 5)
    +    @NonNull
    +    public Token getSplitInfoToken() {
             return mToken;
         }
     
    @@ -127,4 +135,54 @@
                     + ", mToken=" + mToken
                     + '}';
         }
    +
    +    /**
    +     * A unique identifier to represent the split.
    +     */
    +    public static final class Token {
    +
    +        @NonNull
    +        private final IBinder mToken;
    +
    +        Token(@NonNull IBinder token) {
    +            mToken = token;
    +        }
    +
    +        @NonNull
    +        IBinder getRawToken() {
    +            return mToken;
    +        }
    +
    +        @Override
    +        public boolean equals(Object o) {
    +            if (this == o) return true;
    +            if (!(o instanceof Token)) return false;
    +            Token token = (Token) o;
    +            return Objects.equals(mToken, token.mToken);
    +        }
    +
    +        @Override
    +        public int hashCode() {
    +            return Objects.hash(mToken);
    +        }
    +
    +        @NonNull
    +        @Override
    +        public String toString() {
    +            return "Token{"
    +                    + "mToken=" + mToken
    +                    + '}';
    +        }
    +
    +        /**
    +         * Creates a split token from binder.
    +         *
    +         * @param token the raw binder used by OEM Extensions implementation.
    +         */
    +        @RequiresVendorApiLevel(level = 5)
    +        @NonNull
    +        public static Token createFromBinder(@NonNull IBinder token) {
    +            return new Token(token);
    +        }
    +    }
     }
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPinRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPinRule.java
    new file mode 100644
    index 0000000..09932fc8
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPinRule.java
    
    @@ -0,0 +1,145 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.view.WindowMetrics;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +import androidx.window.extensions.core.util.function.Predicate;
    +
    +import java.util.Objects;
    +
    +/**
    + * Split configuration rules for keeping an {@link ActivityStack} in the split in a pin state to
    + * provide an isolated Activity navigation from the split. A pin state here is referring the
    + * {@link ActivityStack} to be fixed on top.
    + *
    + * @see ActivityEmbeddingComponent#pinTopActivityStack
    + */
    +@RequiresVendorApiLevel(level = 5)
    +public class SplitPinRule extends SplitRule {
    +    /**
    +     * Whether the rule should be applied whenever the parent Task satisfied the parent window
    +     * metrics predicate. See {@link ActivityEmbeddingComponent#pinTopActivityStack}.
    +     */
    +    private final boolean mIsSticky;
    +
    +    SplitPinRule(@NonNull SplitAttributes defaultSplitAttributes,
    +            @NonNull Predicate parentWindowMetricsPredicate,
    +            boolean isSticky, @Nullable String tag) {
    +        super(parentWindowMetricsPredicate, defaultSplitAttributes, tag);
    +        mIsSticky = isSticky;
    +    }
    +
    +    /**
    +     * Whether the rule is sticky. This configuration rule can only be applied once when
    +     * possible. That is, the rule will be abandoned whenever the pinned {@link ActivityStack} no
    +     * longer able to be split with another {@link ActivityStack} once the configuration of the
    +     * parent Task is changed. Sets the rule to be sticky if the rule should be permanent until
    +     * the {@link ActivityStack} explicitly unpin.
    +     *
    +     * @see ActivityEmbeddingComponent#pinTopActivityStack
    +     */
    +    public boolean isSticky() {
    +        return mIsSticky;
    +    }
    +
    +    /**
    +     * Builder for {@link SplitPinRule}.
    +     */
    +    public static final class Builder {
    +        @NonNull
    +        private final SplitAttributes mDefaultSplitAttributes;
    +        @NonNull
    +        private final Predicate mParentWindowMetricsPredicate;
    +        private boolean mIsSticky;
    +        @Nullable
    +        private String mTag;
    +
    +        /**
    +         * The {@link SplitPinRule} builder constructor.
    +         *
    +         * @param defaultSplitAttributes the default {@link SplitAttributes} to apply
    +         * @param parentWindowMetricsPredicate the {@link Predicate} to verify if the pinned
    +         *                                     {@link ActivityStack} and the one behind are
    +         *                                     allowed to show adjacent to each other with the
    +         *                                     given parent {@link WindowMetrics}
    +         */
    +        public Builder(@NonNull SplitAttributes defaultSplitAttributes,
    +                @NonNull Predicate parentWindowMetricsPredicate) {
    +            mDefaultSplitAttributes = defaultSplitAttributes;
    +            mParentWindowMetricsPredicate = parentWindowMetricsPredicate;
    +        }
    +
    +        /**
    +         * Sets the rule to be sticky.
    +         *
    +         * @see SplitPinRule#isSticky()
    +         */
    +        @NonNull
    +        public Builder setSticky(boolean isSticky) {
    +            mIsSticky = isSticky;
    +            return this;
    +        }
    +
    +        /**
    +         * @see SplitPinRule#getTag()
    +         */
    +        @NonNull
    +        public Builder setTag(@NonNull String tag) {
    +            mTag =  Objects.requireNonNull(tag);
    +            return this;
    +        }
    +
    +        /**
    +         * Builds a new instance of {@link SplitPinRule}.
    +         */
    +        @NonNull
    +        public SplitPinRule build() {
    +            return new SplitPinRule(mDefaultSplitAttributes, mParentWindowMetricsPredicate,
    +                    mIsSticky, mTag);
    +        }
    +    }
    +
    +    @Override
    +    public boolean equals(Object o) {
    +        if (this == o) return true;
    +        if (!(o instanceof SplitPinRule)) return false;
    +        SplitPinRule that = (SplitPinRule) o;
    +        return super.equals(o)
    +                && mIsSticky == that.mIsSticky;
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        int result = super.hashCode();
    +        result = 31 * result + (mIsSticky ? 1 : 0);
    +        return result;
    +    }
    +
    +    @NonNull
    +    @Override
    +    public String toString() {
    +        return "SplitPinRule{"
    +                + "mTag=" + getTag()
    +                + ", mDefaultSplitAttributes=" + getDefaultSplitAttributes()
    +                + ", mIsSticky=" + mIsSticky
    +                + '}';
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
    index 9f2b5e9..29d60b7 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
    
    @@ -129,7 +129,6 @@
          * Determines what happens with the primary container when all activities are finished in the
          * associated secondary/placeholder container.
          */
    -    // TODO(b/238905747): Add api guard for extensions.
         @RequiresVendorApiLevel(level = 2)
         @SplitPlaceholderFinishBehavior
         public int getFinishPrimaryWithPlaceholder() {
    @@ -270,7 +269,6 @@
             /**
              * @see SplitPlaceholderRule#getFinishPrimaryWithPlaceholder()
              */
    -        // TODO(b/238905747): Add api guard for extensions.
             @RequiresVendorApiLevel(level = 2)
             @NonNull
             public Builder setFinishPrimaryWithPlaceholder(
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/WindowAttributes.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/WindowAttributes.java
    new file mode 100644
    index 0000000..8f3d6ad
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/WindowAttributes.java
    
    @@ -0,0 +1,98 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import androidx.annotation.IntDef;
    +import androidx.annotation.NonNull;
    +import androidx.annotation.Nullable;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +import java.util.Objects;
    +
    +/**
    + * The attributes of the embedded Activity Window.
    + */
    +public final class WindowAttributes {
    +
    +    /**
    +     * The dim effect is applying on the {@link ActivityStack} of the Activity window when
    +     * needed. If the {@link ActivityStack} is not expanded to fill the parent container, the dim
    +     * effect is applying only on the {@link ActivityStack} of
    +     * the requested Activity.
    +     */
    +    public static final int DIM_AREA_ON_ACTIVITY_STACK = 1;
    +
    +    /**
    +     * The dim effect is applying on the area of the whole Task when needed. If the embedded
    +     * transparent activity is split and displayed side-by-side with another activity, the dim
    +     * effect is applying on the Task, which across over the two {@link ActivityStack}s.
    +     */
    +    public static final int DIM_AREA_ON_TASK = 2;
    +
    +    @IntDef({
    +            DIM_AREA_ON_ACTIVITY_STACK,
    +            DIM_AREA_ON_TASK
    +    })
    +    @Retention(RetentionPolicy.SOURCE)
    +    @interface DimAreaBehavior {
    +    }
    +
    +    @DimAreaBehavior
    +    private final int mDimAreaBehavior;
    +
    +    /**
    +     * The {@link WindowAttributes} constructor.
    +     *
    +     * @param dimAreaBehavior the type of area that the dim layer is applying.
    +     */
    +    @RequiresVendorApiLevel(level = 5)
    +    public WindowAttributes(@DimAreaBehavior int dimAreaBehavior) {
    +        mDimAreaBehavior = dimAreaBehavior;
    +    }
    +
    +    /**
    +     * Returns the {@link DimAreaBehavior} to use when dim behind the Activity window is
    +     * needed.
    +     * @return The dim area behavior.
    +     */
    +    @DimAreaBehavior
    +    @RequiresVendorApiLevel(level = 5)
    +    public int getDimAreaBehavior() {
    +        return mDimAreaBehavior;
    +    }
    +
    +    @Override
    +    public boolean equals(@Nullable Object obj) {
    +        if (this == obj) return true;
    +        if (obj == null || !(obj instanceof WindowAttributes)) return false;
    +        final WindowAttributes other = (WindowAttributes) obj;
    +        return mDimAreaBehavior == other.getDimAreaBehavior();
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        return Objects.hash(mDimAreaBehavior);
    +    }
    +
    +    @NonNull
    +    @Override
    +    public String toString() {
    +        return "dimAreaBehavior=" + mDimAreaBehavior;
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/WindowMetricsCompat.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/WindowMetricsCompat.java
    new file mode 100644
    index 0000000..8f99d11
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/WindowMetricsCompat.java
    
    @@ -0,0 +1,53 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import android.os.Build;
    +import android.view.WindowMetrics;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RequiresApi;
    +
    +/**
    + * A helper class to access {@link WindowMetrics#toString()} with compatibility.
    + */
    +class WindowMetricsCompat {
    +    private WindowMetricsCompat() {}
    +
    +    @NonNull
    +    static String toString(@NonNull WindowMetrics windowMetrics) {
    +        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    +            // WindowMetrics#toString is implemented in U.
    +            return windowMetrics.toString();
    +        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    +            return Api30Impl.toString(windowMetrics);
    +        }
    +        // Should be safe since ActivityEmbedding is not enabled before R.
    +        throw new UnsupportedOperationException("WindowMetrics didn't exist in R.");
    +    }
    +
    +    @RequiresApi(30)
    +    private static final class Api30Impl {
    +        @NonNull
    +        static String toString(@NonNull WindowMetrics windowMetrics) {
    +            return WindowMetrics.class.getSimpleName() + ":{"
    +                    + "bounds=" + windowMetrics.getBounds()
    +                    + ", windowInsets=" + windowMetrics.getWindowInsets()
    +                    + "}";
    +        }
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java
    new file mode 100644
    index 0000000..17bb15a
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/DisplayFoldFeature.java
    
    @@ -0,0 +1,223 @@
    +/*
    + * 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 androidx.window.extensions.layout;
    +
    +import android.annotation.SuppressLint;
    +import android.content.Context;
    +import android.os.Build;
    +
    +import androidx.annotation.IntDef;
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RestrictTo;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +import androidx.window.extensions.core.util.function.Consumer;
    +import androidx.window.extensions.util.SetUtilApi23;
    +
    +import java.lang.annotation.ElementType;
    +import java.lang.annotation.Retention;
    +import java.lang.annotation.RetentionPolicy;
    +import java.lang.annotation.Target;
    +import java.util.HashSet;
    +import java.util.Objects;
    +import java.util.Set;
    +
    +/**
    + * Represents a fold on a display that may intersect a window. The presence of a fold does not
    + * imply that it intersects the window an {@link android.app.Activity} is running in. For
    + * example, on a device that can fold like a book and has an outer screen, the fold should be
    + * reported regardless of the folding state, or which screen is on to indicate that there may
    + * be a fold when the user opens the device.
    + *
    + * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to listen to features
    + * that affect the window.
    + */
    +public final class DisplayFoldFeature {
    +
    +    /**
    +     * The type of fold is unknown. This is here for compatibility reasons if a new type is added,
    +     * and cannot be reported to an incompatible application.
    +     */
    +    public static final int TYPE_UNKNOWN = 0;
    +
    +    /**
    +     * The type of fold is a physical hinge separating two display panels.
    +     */
    +    public static final int TYPE_HINGE = 1;
    +
    +    /**
    +     * The type of fold is a screen that folds from 0-180.
    +     */
    +    public static final int TYPE_SCREEN_FOLD_IN = 2;
    +
    +    @RestrictTo(RestrictTo.Scope.LIBRARY)
    +    @Retention(RetentionPolicy.SOURCE)
    +    @IntDef(value = {TYPE_UNKNOWN, TYPE_HINGE, TYPE_SCREEN_FOLD_IN})
    +    public @interface FoldType {
    +    }
    +
    +    /**
    +     * The fold supports the half opened state.
    +     */
    +    public static final int FOLD_PROPERTY_SUPPORTS_HALF_OPENED = 1;
    +
    +    @Target(ElementType.TYPE_USE)
    +    @RestrictTo(RestrictTo.Scope.LIBRARY)
    +    @Retention(RetentionPolicy.SOURCE)
    +    @IntDef(value = {FOLD_PROPERTY_SUPPORTS_HALF_OPENED})
    +    public @interface FoldProperty {
    +    }
    +
    +    @FoldType
    +    private final int mType;
    +
    +    private final Set<@FoldProperty Integer> mProperties;
    +
    +    /**
    +     * Creates an instance of [FoldDisplayFeature].
    +     *
    +     * @param type                  the type of fold, either [FoldDisplayFeature.TYPE_HINGE] or
    +     *                              [FoldDisplayFeature.TYPE_FOLDABLE_SCREEN]
    +     */
    +    DisplayFoldFeature(@FoldType int type, @NonNull Set<@FoldProperty Integer> properties) {
    +        mType = type;
    +        if (Build.VERSION.SDK_INT >= 23) {
    +            mProperties = SetUtilApi23.createSet();
    +        } else {
    +            mProperties = new HashSet<>();
    +        }
    +        mProperties.addAll(properties);
    +    }
    +
    +    /**
    +     * Returns the type of fold that is either a hinge or a fold.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @FoldType
    +    public int getType() {
    +        return mType;
    +    }
    +
    +    /**
    +     * Returns {@code true} if the fold has the given property, {@code false} otherwise.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public boolean hasProperty(@FoldProperty int property) {
    +        return mProperties.contains(property);
    +    }
    +    /**
    +     * Returns {@code true} if the fold has all the given properties, {@code false} otherwise.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    public boolean hasProperties(@NonNull @FoldProperty int... properties) {
    +        for (int i = 0; i < properties.length; i++) {
    +            if (!mProperties.contains(properties[i])) {
    +                return false;
    +            }
    +        }
    +        return true;
    +    }
    +
    +    @Override
    +    public boolean equals(Object o) {
    +        if (this == o) return true;
    +        if (o == null || getClass() != o.getClass()) return false;
    +        DisplayFoldFeature that = (DisplayFoldFeature) o;
    +        return mType == that.mType && Objects.equals(mProperties, that.mProperties);
    +    }
    +
    +    @Override
    +    public int hashCode() {
    +        return Objects.hash(mType, mProperties);
    +    }
    +
    +    @Override
    +    @NonNull
    +    public String toString() {
    +        return "ScreenFoldDisplayFeature{mType=" + mType + ", mProperties=" + mProperties + '}';
    +    }
    +
    +    /**
    +     * A builder to construct an instance of {@link DisplayFoldFeature}.
    +     */
    +    public static final class Builder {
    +
    +        @FoldType
    +        private int mType;
    +
    +        private Set<@FoldProperty Integer> mProperties;
    +
    +        /**
    +         * Constructs a builder to create an instance of {@link DisplayFoldFeature}.
    +         *
    +         * @param type                  the type of hinge for the {@link DisplayFoldFeature}.
    +         * @see DisplayFoldFeature.FoldType
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder(@FoldType int type) {
    +            mType = type;
    +            if (Build.VERSION.SDK_INT >= 23) {
    +                mProperties = SetUtilApi23.createSet();
    +            } else {
    +                mProperties = new HashSet<>();
    +            }
    +        }
    +
    +        /**
    +         * Add a property to the set of properties exposed by {@link DisplayFoldFeature}.
    +         */
    +        @SuppressLint("MissingGetterMatchingBuilder")
    +        @NonNull
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder addProperty(@FoldProperty int property) {
    +            mProperties.add(property);
    +            return this;
    +        }
    +
    +        /**
    +         * Add a list of properties to the set of properties exposed by
    +         * {@link DisplayFoldFeature}.
    +         */
    +        @SuppressLint("MissingGetterMatchingBuilder")
    +        @NonNull
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder addProperties(@NonNull @FoldProperty int... properties) {
    +            for (int i = 0; i < properties.length; i++) {
    +                mProperties.add(properties[i]);
    +            }
    +            return this;
    +        }
    +
    +        /**
    +         * Clear the properties in the builder.
    +         */
    +        @NonNull
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder clearProperties() {
    +            mProperties.clear();
    +            return this;
    +        }
    +
    +        /**
    +         * Returns an instance of {@link DisplayFoldFeature}.
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        @NonNull
    +        public DisplayFoldFeature build() {
    +            return new DisplayFoldFeature(mType, mProperties);
    +        }
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java
    new file mode 100644
    index 0000000..2143e29
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/SupportedWindowFeatures.java
    
    @@ -0,0 +1,72 @@
    +/*
    + * 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 androidx.window.extensions.layout;
    +
    +import androidx.annotation.NonNull;
    +import androidx.window.extensions.RequiresVendorApiLevel;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +/**
    + * A class to represent all the possible features that may interact with or appear in a window,
    + * that an application might want to respond to.
    + */
    +public final class SupportedWindowFeatures {
    +
    +    private final List mDisplayFoldFeatureList;
    +
    +    private SupportedWindowFeatures(
    +            @NonNull List displayFoldFeatureList) {
    +        mDisplayFoldFeatureList = new ArrayList<>(displayFoldFeatureList);
    +    }
    +
    +    /**
    +     * Returns the possible {@link DisplayFoldFeature}s that can be reported to an application.
    +     */
    +    @NonNull
    +    @RequiresVendorApiLevel(level = 6)
    +    public List getDisplayFoldFeatures() {
    +        return new ArrayList<>(mDisplayFoldFeatureList);
    +    }
    +
    +
    +    /**
    +     * A class to create a {@link SupportedWindowFeatures} instance.
    +     */
    +    public static final class Builder {
    +
    +        private final List mDisplayFoldFeatures;
    +
    +        /**
    +         * Creates a new instance of {@link Builder}
    +         */
    +        @RequiresVendorApiLevel(level = 6)
    +        public Builder(@NonNull List displayFoldFeatures) {
    +            mDisplayFoldFeatures = new ArrayList<>(displayFoldFeatures);
    +        }
    +
    +        /**
    +         * Creates an instance of {@link SupportedWindowFeatures} with the features set.
    +         */
    +        @NonNull
    +        @RequiresVendorApiLevel(level = 6)
    +        public SupportedWindowFeatures build() {
    +            return new SupportedWindowFeatures(mDisplayFoldFeatures);
    +        }
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
    index 875f763..d55e341 100644
    --- a/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/layout/WindowLayoutComponent.java
    
    @@ -96,4 +96,17 @@
             throw new UnsupportedOperationException("This method must not be called unless there is a"
                     + " corresponding override implementation on the device.");
         }
    +
    +    /**
    +     * Returns the {@link SupportedWindowFeatures} for the device. This value will not change
    +     * over time.
    +     * @see WindowLayoutComponent#addWindowLayoutInfoListener(Context, Consumer) to register a
    +     * listener for features that impact the window.
    +     */
    +    @RequiresVendorApiLevel(level = 6)
    +    @NonNull
    +    default SupportedWindowFeatures getSupportedWindowFeatures() {
    +        throw new UnsupportedOperationException("This method will not be called unless there is a"
    +                + " corresponding override implementation on the device");
    +    }
     }
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/util/SetCompat.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/util/SetCompat.java
    new file mode 100644
    index 0000000..15305f2d
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/util/SetCompat.java
    
    @@ -0,0 +1,60 @@
    +/*
    + * 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 androidx.window.extensions.util;
    +
    +import android.os.Build;
    +import android.util.ArraySet;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RequiresApi;
    +import androidx.annotation.RestrictTo;
    +
    +import java.util.HashSet;
    +import java.util.Set;
    +
    +/**
    + * A {@link Set} wrapper for compatibility. It {@link ArraySet} if it's available, and uses
    + * other compatible {@link Set} class, otherwise.
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +public final class SetCompat {
    +
    +    private SetCompat() {}
    +
    +    /**
    +     * Creates a {@link Set}.
    +     *
    +     * @param  the type of the {@link Set}.
    +     */
    +    @NonNull
    +    public static  Set create() {
    +        if (Build.VERSION.SDK_INT < 23) {
    +            return new HashSet<>();
    +        } else {
    +            return Api23Impl.create();
    +        }
    +    }
    +
    +    @RequiresApi(23)
    +    private static class Api23Impl {
    +
    +        @NonNull
    +        static  Set create() {
    +            return new ArraySet<>();
    +        }
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/util/SetUtilApi23.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/util/SetUtilApi23.java
    new file mode 100644
    index 0000000..c7896a1
    --- /dev/null
    +++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/util/SetUtilApi23.java
    
    @@ -0,0 +1,43 @@
    +/*
    + * 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 androidx.window.extensions.util;
    +
    +import android.util.ArraySet;
    +
    +import androidx.annotation.NonNull;
    +import androidx.annotation.RequiresApi;
    +import androidx.annotation.RestrictTo;
    +
    +import java.util.Set;
    +
    +/**
    + * Set utilities for creating working with newer {@link ArraySet} apis.
    + */
    +@RestrictTo(RestrictTo.Scope.LIBRARY)
    +@RequiresApi(23)
    +public final class SetUtilApi23 {
    +
    +    private SetUtilApi23() {}
    +
    +    /**
    +     * Creates an instance of {@link ArraySet}.
    +     */
    +    @NonNull
    +    public static  Set createSet() {
    +        return new ArraySet<>();
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/AnimationBackgroundTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/AnimationBackgroundTest.java
    new file mode 100644
    index 0000000..b4475e9
    --- /dev/null
    +++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/AnimationBackgroundTest.java
    
    @@ -0,0 +1,58 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertNotEquals;
    +import static org.junit.Assert.assertThrows;
    +
    +import android.graphics.Color;
    +
    +import androidx.test.filters.SmallTest;
    +
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.robolectric.RobolectricTestRunner;
    +
    +/** Test for {@link AnimationBackground} */
    +@SmallTest
    +@RunWith(RobolectricTestRunner.class)
    +public class AnimationBackgroundTest {
    +
    +    @Test
    +    public void testDefaultBackground() {
    +        assertEquals(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT,
    +                AnimationBackground.ANIMATION_BACKGROUND_DEFAULT);
    +        assertFalse(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT
    +                instanceof AnimationBackground.ColorBackground);
    +    }
    +
    +    @Test
    +    public void testCreateColorBackground() {
    +        final AnimationBackground.ColorBackground background =
    +                AnimationBackground.createColorBackground(Color.BLUE);
    +
    +        assertEquals(Color.BLUE, background.getColor());
    +        assertEquals(background, AnimationBackground.createColorBackground(
    +                Color.BLUE));
    +        assertNotEquals(background, AnimationBackground.createColorBackground(
    +                Color.GREEN));
    +        assertThrows(IllegalArgumentException.class,
    +                () -> AnimationBackground.createColorBackground(Color.TRANSPARENT));
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java
    new file mode 100644
    index 0000000..69d0784
    --- /dev/null
    +++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/DividerAttributesTest.java
    
    @@ -0,0 +1,167 @@
    +/*
    + * 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 androidx.window.extensions.embedding;
    +
    +import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_DRAGGABLE;
    +import static androidx.window.extensions.embedding.DividerAttributes.DIVIDER_TYPE_FIXED;
    +import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT;
    +import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT;
    +
    +import static com.google.common.truth.Truth.assertThat;
    +
    +import static org.junit.Assert.assertThrows;
    +
    +import androidx.test.filters.SmallTest;
    +
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.robolectric.RobolectricTestRunner;
    +
    +/** Verifies {@link DividerAttributes} behavior. */
    +@SmallTest
    +@RunWith(RobolectricTestRunner.class)
    +public class DividerAttributesTest {
    +
    +    @Test
    +    public void testDividerAttributesDefaults() {
    +        final DividerAttributes defaultAttrs =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_FIXED).build();
    +        assertThat(defaultAttrs.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);
    +        assertThat(defaultAttrs.getWidthDp()).isEqualTo(WIDTH_SYSTEM_DEFAULT);
    +        assertThat(defaultAttrs.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +        assertThat(defaultAttrs.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +    }
    +
    +    @Test
    +    public void testDividerAttributesBuilder() {
    +        final DividerAttributes dividerAttributes1 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
    +                        .setWidthDp(20)
    +                        .build();
    +        assertThat(dividerAttributes1.getDividerType()).isEqualTo(DIVIDER_TYPE_FIXED);
    +        assertThat(dividerAttributes1.getWidthDp()).isEqualTo(20);
    +        assertThat(dividerAttributes1.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +        assertThat(dividerAttributes1.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +
    +        final DividerAttributes dividerAttributes2 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(20)
    +                        .setPrimaryMinRatio(0.2f)
    +                        .setPrimaryMaxRatio(0.8f)
    +                        .build();
    +        assertThat(dividerAttributes2.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
    +        assertThat(dividerAttributes2.getWidthDp()).isEqualTo(20);
    +        assertThat(dividerAttributes2.getPrimaryMinRatio()).isEqualTo(0.2f);
    +        assertThat(dividerAttributes2.getPrimaryMaxRatio()).isEqualTo(0.8f);
    +
    +        final DividerAttributes dividerAttributes3 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(20)
    +                        .build();
    +        assertThat(dividerAttributes3.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
    +        assertThat(dividerAttributes3.getWidthDp()).isEqualTo(20);
    +        assertThat(dividerAttributes3.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +        assertThat(dividerAttributes3.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +
    +        final DividerAttributes dividerAttributes4 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(20)
    +                        .setPrimaryMinRatio(0.2f)
    +                        .build();
    +        assertThat(dividerAttributes4.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
    +        assertThat(dividerAttributes4.getWidthDp()).isEqualTo(20);
    +        assertThat(dividerAttributes4.getPrimaryMinRatio()).isEqualTo(0.2f);
    +        assertThat(dividerAttributes4.getPrimaryMaxRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +
    +        final DividerAttributes dividerAttributes5 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(20)
    +                        .setPrimaryMaxRatio(0.2f)
    +                        .build();
    +        assertThat(dividerAttributes5.getDividerType()).isEqualTo(DIVIDER_TYPE_DRAGGABLE);
    +        assertThat(dividerAttributes5.getWidthDp()).isEqualTo(20);
    +        assertThat(dividerAttributes5.getPrimaryMinRatio()).isEqualTo(RATIO_SYSTEM_DEFAULT);
    +        assertThat(dividerAttributes5.getPrimaryMaxRatio()).isEqualTo(0.2f);
    +    }
    +
    +    @Test
    +    public void testDividerAttributesEquals() {
    +        final DividerAttributes dividerAttributes1 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(20)
    +                        .setPrimaryMinRatio(0.2f)
    +                        .setPrimaryMaxRatio(0.8f)
    +                        .build();
    +
    +        final DividerAttributes dividerAttributes2 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(20)
    +                        .setPrimaryMinRatio(0.2f)
    +                        .setPrimaryMaxRatio(0.8f)
    +                        .build();
    +
    +        final DividerAttributes dividerAttributes3 =
    +                new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
    +                        .setWidthDp(20)
    +                        .build();
    +
    +        assertThat(dividerAttributes1).isEqualTo(dividerAttributes2);
    +        assertThat(dividerAttributes1).isNotEqualTo(dividerAttributes3);
    +    }
    +
    +    @Test
    +    public void testDividerAttributesValidation() {
    +        assertThrows(
    +                "Must not set min max ratio for DIVIDER_TYPE_FIXED",
    +                IllegalStateException.class,
    +                () -> new DividerAttributes.Builder(DIVIDER_TYPE_FIXED)
    +                        .setPrimaryMinRatio(0.2f)
    +                        .setPrimaryMaxRatio(0.8f)
    +                        .build()
    +        );
    +
    +        assertThrows(
    +                "Min ratio must be less than or equal to max ratio",
    +                IllegalStateException.class,
    +                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setPrimaryMinRatio(0.8f)
    +                        .setPrimaryMaxRatio(0.2f)
    +                        .build()
    +        );
    +
    +        assertThrows(
    +                "Min ratio must be in range [0.0, 1.0] or RATIO_UNSET",
    +                IllegalArgumentException.class,
    +                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setPrimaryMinRatio(2.0f)
    +        );
    +
    +        assertThrows(
    +                "Max ratio must be in range [0.0, 1.0] or RATIO_UNSET",
    +                IllegalArgumentException.class,
    +                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setPrimaryMaxRatio(2.0f)
    +        );
    +
    +        assertThrows(
    +                "Width must be greater than or equal to zero or WIDTH_UNSET",
    +                IllegalArgumentException.class,
    +                () -> new DividerAttributes.Builder(DIVIDER_TYPE_DRAGGABLE)
    +                        .setWidthDp(-10)
    +        );
    +    }
    +}
    
    diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
    index 7886c84..f6d34fa 100644
    --- a/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
    +++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
    
    @@ -36,46 +36,57 @@
     public class SplitAttributesTest {
         @Test
         public void testSplitAttributesEquals() {
    -        final SplitAttributes layout1 = new SplitAttributes.Builder()
    +        final SplitAttributes attrs1 = new SplitAttributes.Builder()
                     .setSplitType(splitEqually())
                     .setLayoutDirection(LayoutDirection.LOCALE)
    -                .setAnimationBackgroundColor(SplitAttributes.DEFAULT_ANIMATION_BACKGROUND_COLOR)
    +                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
                     .build();
    -        final SplitAttributes layout2 = new SplitAttributes.Builder()
    +        final SplitAttributes attrs2 = new SplitAttributes.Builder()
                     .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                     .setLayoutDirection(LayoutDirection.LOCALE)
    -                .setAnimationBackgroundColor(SplitAttributes.DEFAULT_ANIMATION_BACKGROUND_COLOR)
    +                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
                     .build();
    -        final SplitAttributes layout3 = new SplitAttributes.Builder()
    +        final SplitAttributes attrs3 = new SplitAttributes.Builder()
                     .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                     .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
    -                .setAnimationBackgroundColor(SplitAttributes.DEFAULT_ANIMATION_BACKGROUND_COLOR)
    +                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
                     .build();
    -        final SplitAttributes layout4 = new SplitAttributes.Builder()
    +        final SplitAttributes attrs4 = new SplitAttributes.Builder()
                     .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                     .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
    -                .setAnimationBackgroundColor(Color.BLUE)
    +                .setAnimationBackground(AnimationBackground.createColorBackground(Color.BLUE))
                     .build();
    -        final SplitAttributes layout5 = new SplitAttributes.Builder()
    +        final SplitAttributes attrs5 = new SplitAttributes.Builder()
                     .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
                     .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
    -                .setAnimationBackgroundColor(Color.BLUE)
    +                .setAnimationBackground(AnimationBackground.createColorBackground(Color.BLUE))
                     .build();
     
    -        assertNotEquals(layout1, layout2);
    -        assertNotEquals(layout1.hashCode(), layout2.hashCode());
    +        assertNotEquals(attrs1, attrs2);
    +        assertNotEquals(attrs1.hashCode(), attrs2.hashCode());
     
    -        assertNotEquals(layout2, layout3);
    -        assertNotEquals(layout2.hashCode(), layout3.hashCode());
    +        assertNotEquals(attrs2, attrs3);
    +        assertNotEquals(attrs2.hashCode(), attrs3.hashCode());
     
    -        assertNotEquals(layout3, layout1);
    -        assertNotEquals(layout3.hashCode(), layout1.hashCode());
    +        assertNotEquals(attrs3, attrs1);
    +        assertNotEquals(attrs3.hashCode(), attrs1.hashCode());
     
    -        assertNotEquals(layout4, layout3);
    -        assertNotEquals(layout4.hashCode(), layout3.hashCode());
    +        assertNotEquals(attrs4, attrs3);
    +        assertNotEquals(attrs4.hashCode(), attrs3.hashCode());
     
    -        assertEquals(layout4, layout5);
    -        assertEquals(layout4.hashCode(), layout5.hashCode());
    +        assertEquals(attrs4, attrs5);
    +        assertEquals(attrs4.hashCode(), attrs5.hashCode());
    +    }
    +
    +    @Test
    +    public void testSplitAttributesEqualsUsingBuilderFromExistingInstance() {
    +        final SplitAttributes attrs1 = new SplitAttributes.Builder()
    +                .setSplitType(splitEqually())
    +                .setLayoutDirection(LayoutDirection.LOCALE)
    +                .setAnimationBackground(AnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
    +                .build();
    +        final SplitAttributes attrs2 = new SplitAttributes.Builder(attrs1).build();
    +        assertEquals(attrs1, attrs2);
         }
     
         @Test
    
    diff --git a/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java b/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java
    new file mode 100644
    index 0000000..e9937a0
    --- /dev/null
    +++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/layout/DisplayFoldFeatureTest.java
    
    @@ -0,0 +1,67 @@
    +/*
    + * 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 androidx.window.extensions.layout;
    +
    +import static androidx.window.extensions.layout.DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED;
    +import static androidx.window.extensions.layout.DisplayFoldFeature.TYPE_HINGE;
    +
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertTrue;
    +
    +import org.junit.Test;
    +
    +import java.util.HashSet;
    +import java.util.Set;
    +
    +public class DisplayFoldFeatureTest {
    +
    +    @Test
    +    public void test_builder_matches_constructor() {
    +        Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();
    +        properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
    +        DisplayFoldFeature expected = new DisplayFoldFeature(TYPE_HINGE, properties);
    +
    +        DisplayFoldFeature actual = new DisplayFoldFeature.Builder(TYPE_HINGE)
    +                .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
    +                .build();
    +
    +        assertEquals(expected, actual);
    +    }
    +
    +    @Test
    +    public void test_equals_matches() {
    +        Set<@DisplayFoldFeature.FoldProperty Integer> properties = new HashSet<>();
    +        properties.add(FOLD_PROPERTY_SUPPORTS_HALF_OPENED);
    +        DisplayFoldFeature first = new DisplayFoldFeature(TYPE_HINGE, properties);
    +        DisplayFoldFeature second = new DisplayFoldFeature(TYPE_HINGE, properties);
    +
    +        assertEquals(first, second);
    +        assertEquals(first.hashCode(), second.hashCode());
    +    }
    +
    +    @Test
    +    public void test_getter_matches_values() {
    +        final int type = TYPE_HINGE;
    +        DisplayFoldFeature actual = new DisplayFoldFeature.Builder(type)
    +                .addProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
    +                .build();
    +
    +        assertEquals(type, actual.getType());
    +        assertTrue(actual.hasProperty(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));
    +        assertTrue(actual.hasProperties(FOLD_PROPERTY_SUPPORTS_HALF_OPENED));
    +    }
    +}
    
    diff --git a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
    index 572681b..0652ed8 100644
    --- a/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
    +++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
    
    @@ -52,6 +52,11 @@
             ++id
         }
     
    +    fun appendAndNotify(title: String, message: String) {
    +        append(title, message)
    +        notifyDataSetChanged()
    +    }
    +
         private fun append(item: InfoLog) {
             items.add(0, item)
         }
    
    diff --git a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
    index 15d5101..177762b 100644
    --- a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
    +++ b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
    
    @@ -17,6 +17,7 @@
             android:supportsRtl="true">
             
                 android:name=".embedding.TrustedEmbeddingActivity"
    +            android:supportsPictureInPicture="true"
                 android:exported="true"
                 android:label="@string/trusted_embedding_activity"
                 android:configChanges=
    @@ -30,6 +31,7 @@
             
             
                 android:name=".embedding.UntrustedEmbeddingActivity"
    +            android:supportsPictureInPicture="true"
                 android:exported="true"
                 android:label="@string/untrusted_embedding_activity"
                 android:configChanges=
    @@ -39,6 +41,11 @@
                     
                     
                 
    +            
    +            
    +                android:name="android.window.PROPERTY_ALLOW_UNTRUSTED_ACTIVITY_EMBEDDING_STATE_SHARING"
    +                android:value="true" />
             
             
                 android:name="androidx.window.demo2.DisplayFeaturesActivity"
    
    diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt
    new file mode 100644
    index 0000000..e20e120
    --- /dev/null
    +++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/EmbeddedActivityBase.kt
    
    @@ -0,0 +1,92 @@
    +/*
    + * 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 androidx.window.demo2.embedding
    +
    +import android.content.Intent
    +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
    +import android.os.Bundle
    +import android.widget.TextView
    +import androidx.appcompat.app.AppCompatActivity
    +import androidx.lifecycle.Lifecycle
    +import androidx.lifecycle.lifecycleScope
    +import androidx.lifecycle.repeatOnLifecycle
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.demo.common.util.PictureInPictureUtil
    +import androidx.window.demo2.R
    +import androidx.window.demo2.databinding.ActivityEmbeddedBinding
    +import androidx.window.embedding.ActivityEmbeddingController
    +import kotlinx.coroutines.Dispatchers
    +import kotlinx.coroutines.launch
    +
    +open class EmbeddedActivityBase : AppCompatActivity() {
    +    lateinit var viewBinding: ActivityEmbeddedBinding
    +    private lateinit var activityEmbeddingController: ActivityEmbeddingController
    +    private lateinit var windowInfoView: TextView
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +        viewBinding = ActivityEmbeddedBinding.inflate(layoutInflater)
    +        setContentView(viewBinding.root)
    +        viewBinding.buttonPip.setOnClickListener {
    +            PictureInPictureUtil.startPictureInPicture(this, false)
    +        }
    +        viewBinding.buttonStartActivity.setOnClickListener {
    +            startActivity(Intent(this, this.javaClass))
    +        }
    +        viewBinding.buttonStartActivityFromApplicationContext.setOnClickListener {
    +            application.startActivity(
    +                Intent(this, this.javaClass).setFlags(FLAG_ACTIVITY_NEW_TASK)
    +            )
    +        }
    +
    +        activityEmbeddingController = ActivityEmbeddingController.getInstance(this)
    +        initializeEmbeddedActivityInfoCallback()
    +    }
    +
    +    private fun initializeEmbeddedActivityInfoCallback() {
    +        val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
    +        if (extensionVersion < 6) {
    +            // EmbeddedActivityWindowInfo is only available on 6+.
    +            return
    +        }
    +
    +        windowInfoView = viewBinding.windowIntoText
    +        lifecycleScope.launch(Dispatchers.Main) {
    +            // Collect EmbeddedActivityWindowInfo when STARTED and stop when STOPPED.
    +            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
    +                // After register, the flow will be triggered immediately if the activity is
    +                // embedded.
    +                // However, if the activity is changed to non-embedded state in background (after
    +                // STOPPED), the flow will not report the change (because it has been unregistered).
    +                // Reset before start listening.
    +                resetWindowInfoView()
    +                activityEmbeddingController.embeddedActivityWindowInfo(this@EmbeddedActivityBase)
    +                    .collect { info ->
    +                        if (info.isEmbedded) {
    +                            windowInfoView.text = info.toString()
    +                        } else {
    +                            resetWindowInfoView()
    +                        }
    +                    }
    +            }
    +        }
    +    }
    +
    +    private fun resetWindowInfoView() {
    +        windowInfoView.text = getString(R.string.embedded_window_info_unavailable)
    +    }
    +}
    
    diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
    index 67c0b09..1a2ea23 100644
    --- a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
    +++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
    
    @@ -16,9 +16,7 @@
     
     package androidx.window.demo2.embedding
     
    -import android.app.Activity
     import android.os.Bundle
    -import android.widget.TextView
     import androidx.window.demo2.R
     
     /**
    @@ -26,12 +24,10 @@
      * `android:allowUntrustedActivityEmbedding` in AndroidManifest. Activity can be launched from the
      * split demos in window-samples/demos.
      */
    -class TrustedEmbeddingActivity : Activity() {
    +class TrustedEmbeddingActivity : EmbeddedActivityBase() {
     
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        setContentView(R.layout.activity_embedded)
    -        findViewById(R.id.detail_text_view).text =
    -            getString(R.string.trusted_embedding_activity_detail)
    +        viewBinding.detailTextView.text = getString(R.string.trusted_embedding_activity_detail)
         }
     }
    
    diff --git a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
    index cfe7c21..9125124 100644
    --- a/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
    +++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
    
    @@ -16,9 +16,7 @@
     
     package androidx.window.demo2.embedding
     
    -import android.app.Activity
     import android.os.Bundle
    -import android.widget.TextView
     import androidx.window.demo2.R
     
     /**
    @@ -26,12 +24,10 @@
      * `android:allowUntrustedActivityEmbedding` in AndroidManifest. Activity can be launched from
      * the split demos in window-samples/demos.
      */
    -class UntrustedEmbeddingActivity : Activity() {
    +class UntrustedEmbeddingActivity : EmbeddedActivityBase() {
     
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        setContentView(R.layout.activity_embedded)
    -        findViewById(R.id.detail_text_view).text =
    -            getString(R.string.untrusted_embedding_activity_detail)
    +        viewBinding.detailTextView.text = getString(R.string.untrusted_embedding_activity_detail)
         }
     }
    
    diff --git a/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
    index fbd572c..102145e 100644
    --- a/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
    +++ b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
    
    @@ -15,9 +15,13 @@
       limitations under the License.
       -->
     
    -
    +
    +    xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
    -    android:layout_height="match_parent">
    +    android:layout_height="match_parent"
    +    android:orientation="vertical"
    +    android:padding="10dp"
    +    android:background="@color/colorAccent">
     
         
             android:layout_gravity="center"
    @@ -25,4 +29,44 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
     
    -
    \ No newline at end of file
    +    
    +        android:layout_width="match_parent"
    +        android:layout_height="1dp"
    +        android:layout_marginTop="10dp"
    +        android:layout_marginBottom="10dp"
    +        android:background="#AAAAAA" />
    +
    +    
    +        android:layout_gravity="center"
    +        android:id="@+id/window_into_text"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:text="@string/embedded_window_info_unavailable"/>
    +
    +    
    +        android:layout_width="match_parent"
    +        android:layout_height="1dp"
    +        android:layout_marginTop="10dp"
    +        android:layout_marginBottom="10dp"
    +        android:background="#AAAAAA" />
    +
    +    
    +        android:id="@+id/button_pip"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:layout_gravity="center"
    +        android:text="ENTER PIP" />
    +    
    +        android:id="@+id/button_start_activity"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:layout_gravity="center"
    +        android:text="Start a new instance of current Activity" />
    +    
    +        android:id="@+id/button_start_activity_from_application_context"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:layout_gravity="center"
    +        android:text="Start a new instance of current Activity from Application Context" />
    +
    +
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo-second-app/src/main/res/values/strings.xml b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
    index de4172b..7fa3d6d 100644
    --- a/window/window-demos/demo-second-app/src/main/res/values/strings.xml
    +++ b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
    
    @@ -22,4 +22,6 @@
         Untrusted Embedding Activity
         Activity allows embedding in untrusted mode
             via opt-in.
    +    EmbeddedActivityWindowInfo not available
    +    
     
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo/src/main/AndroidManifest.xml b/window/window-demos/demo/src/main/AndroidManifest.xml
    index 5b81535..23add8e 100644
    --- a/window/window-demos/demo/src/main/AndroidManifest.xml
    +++ b/window/window-demos/demo/src/main/AndroidManifest.xml
    
    @@ -84,6 +84,10 @@
                     
                 
             
    +        
    +            android:exported="false"
    +            android:configChanges="orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
    +            android:label="@string/dual_display" />
             
                 android:name=".embedding.SplitActivityA"
                 android:exported="true"
    @@ -132,10 +136,17 @@
                 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
                 android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
             
    +            android:name=".embedding.DialogActivity"
    +            android:theme="@style/Theme.AppCompat.Dialog"
    +            android:exported="false"
    +            android:label="Dialog Activity"
    +            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
    +            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
    +        
                 android:name=".embedding.ExpandedDialogActivity"
                 android:theme="@style/ExpandedDialogTheme"
                 android:exported="false"
    -            android:label="Dialog Activity"
    +            android:label="Expanded Dialog Activity"
                 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
                 android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
             
    @@ -292,6 +303,32 @@
                 android:taskAffinity="androidx.window.demo.split_ime">
             
     
    +        
    +
    +        
    +            android:name=".embedding.OverlayAssociatedActivityA"
    +            android:exported="true"
    +            android:label="Overlay Activity A"
    +            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
    +            android:taskAffinity="androidx.window.demo.overlay_activity_affinity">
    +            
    +                
    +                
    +            
    +        
    +        
    +            android:name=".embedding.OverlayAssociatedActivityB"
    +            android:exported="true"
    +            android:label="Overlay Activity B"
    +            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
    +            android:taskAffinity="androidx.window.demo.overlay_activity_affinity">
    +        
    +        
    +            android:name=".embedding.SplitWithOverlayActivity"
    +            android:targetActivity=".embedding.SplitActivityDetail"
    +            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
    +            android:taskAffinity="androidx.window.demo.overlay_activity_affinity" />
    +
             
     
             
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
    index ea31885..d8c5b42 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
    
    @@ -75,7 +75,8 @@
     
             val logBuilder = StringBuilder().append("Width: $width, Height: $height\n" +
                 "Top: ${windowMetrics.bounds.top}, Bottom: ${windowMetrics.bounds.bottom}, " +
    -            "Left: ${windowMetrics.bounds.left}, Right: ${windowMetrics.bounds.right}")
    +            "Left: ${windowMetrics.bounds.left}, Right: ${windowMetrics.bounds.right}\n" +
    +            "Density: ${windowMetrics.density}")
     
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                 val windowInsets = windowMetrics.getWindowInsets()
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
    index 4bdc573..3bfafc7 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
    
    @@ -45,7 +45,8 @@
             val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
             val width = windowMetrics.bounds.width()
             val height = windowMetrics.bounds.height()
    -        adapter.append("WindowMetrics update", "width: $width, height: $height")
    +        adapter.append("WindowMetrics update", "width: $width, height: $height, " +
    +            "density: ${windowMetrics.density}")
             adapter.notifyDataSetChanged()
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
    index 7e12657..0f6ea761 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayActivityConfigChanges.kt
    
    @@ -22,15 +22,12 @@
     import androidx.lifecycle.Lifecycle
     import androidx.lifecycle.lifecycleScope
     import androidx.lifecycle.repeatOnLifecycle
    -import androidx.window.area.WindowAreaCapability
     import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_TRANSFER_ACTIVITY_TO_AREA
    -import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNAVAILABLE
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
     import androidx.window.area.WindowAreaController
     import androidx.window.area.WindowAreaInfo
    -import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
     import androidx.window.area.WindowAreaSession
     import androidx.window.area.WindowAreaSessionCallback
     import androidx.window.core.ExperimentalWindowApi
    @@ -41,9 +38,6 @@
     import java.util.Locale
     import java.util.concurrent.Executor
     import kotlinx.coroutines.Dispatchers
    -import kotlinx.coroutines.flow.distinctUntilChanged
    -import kotlinx.coroutines.flow.map
    -import kotlinx.coroutines.flow.onEach
     import kotlinx.coroutines.launch
     
     /**
    @@ -58,11 +52,10 @@
     
         private lateinit var windowAreaController: WindowAreaController
         private var rearDisplaySession: WindowAreaSession? = null
    -    private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
    -    private var rearDisplayStatus: WindowAreaCapability.Status = WINDOW_AREA_STATUS_UNSUPPORTED
         private val infoLogAdapter = InfoLogAdapter()
         private lateinit var binding: ActivityRearDisplayBinding
         private lateinit var executor: Executor
    +    private var currentWindowAreaInfo: WindowAreaInfo? = null
     
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    @@ -74,56 +67,76 @@
     
             binding.rearStatusRecyclerView.adapter = infoLogAdapter
     
    -        binding.rearDisplayButton.setOnClickListener {
    -            if (rearDisplayStatus == WINDOW_AREA_STATUS_ACTIVE) {
    -                if (rearDisplaySession == null) {
    -                    rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(
    -                        OPERATION_TRANSFER_ACTIVITY_TO_AREA
    +        lifecycleScope.launch(Dispatchers.Main) {
    +            // The block passed to repeatOnLifecycle is executed when the lifecycle
    +            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
    +            // It automatically restarts the block when the lifecycle is STARTED again.
    +            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
    +                // Safely collect from windowInfoRepo when the lifecycle is STARTED
    +                // and stops collection when the lifecycle is STOPPED
    +                windowAreaController.windowAreaInfos.collect { windowAreaInfos ->
    +                    infoLogAdapter.appendAndNotify(
    +                        getCurrentTimeString(),
    +                        "number of areas: " + windowAreaInfos.size
                         )
    -                }
    -                rearDisplaySession?.close()
    -            } else {
    -                rearDisplayWindowAreaInfo?.token?.let { token ->
    -                    windowAreaController.transferActivityToWindowArea(
    -                        token = token,
    -                        activity = this,
    -                        executor = executor,
    -                        windowAreaSessionCallback = this)
    +                    windowAreaInfos.forEach { windowAreaInfo ->
    +                        if (windowAreaInfo.type == WindowAreaInfo.Type.TYPE_REAR_FACING) {
    +                            currentWindowAreaInfo = windowAreaInfo
    +                            val transferCapability = windowAreaInfo.getCapability(
    +                                OPERATION_TRANSFER_ACTIVITY_TO_AREA
    +                            )
    +                            infoLogAdapter.append(
    +                                getCurrentTimeString(),
    +                                transferCapability.status.toString() + " : " +
    +                                    windowAreaInfo.metrics.toString()
    +                            )
    +                            updateRearDisplayButton()
    +                        }
    +                    }
    +                    infoLogAdapter.notifyDataSetChanged()
                     }
                 }
             }
     
    -        lifecycleScope.launch(Dispatchers.Main) {
    -            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
    -                windowAreaController
    -                    .windowAreaInfos
    -                    .map { windowAreaInfoList -> windowAreaInfoList.firstOrNull {
    -                        windowAreaInfo -> windowAreaInfo.type == TYPE_REAR_FACING
    -                    } }
    -                    .onEach { windowAreaInfo -> rearDisplayWindowAreaInfo = windowAreaInfo }
    -                    .map(this@RearDisplayActivityConfigChanges::getRearDisplayStatus)
    -                    .distinctUntilChanged()
    -                    .collect { status ->
    -                        infoLogAdapter.append(getCurrentTimeString(), status.toString())
    -                        infoLogAdapter.notifyDataSetChanged()
    -                        rearDisplayStatus = status
    -                        updateRearDisplayButton()
    -                    }
    +        binding.rearDisplayButton.setOnClickListener {
    +            if (rearDisplaySession != null) {
    +                rearDisplaySession?.close()
    +            } else {
    +                currentWindowAreaInfo?.let {
    +                    windowAreaController.transferActivityToWindowArea(
    +                        it.token,
    +                        this,
    +                        executor,
    +                        this
    +                    )
    +                }
    +            }
    +        }
    +
    +        binding.rearDisplaySessionButton.setOnClickListener {
    +            if (rearDisplaySession == null) {
    +                try {
    +                    rearDisplaySession = currentWindowAreaInfo?.getActiveSession(
    +                        OPERATION_TRANSFER_ACTIVITY_TO_AREA
    +                    )
    +                    updateRearDisplayButton()
    +                } catch (e: IllegalStateException) {
    +                    infoLogAdapter.appendAndNotify(getCurrentTimeString(), e.toString())
    +                }
                 }
             }
         }
     
         override fun onSessionStarted(session: WindowAreaSession) {
             rearDisplaySession = session
    -        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
    -        infoLogAdapter.notifyDataSetChanged()
    +        infoLogAdapter.appendAndNotify(getCurrentTimeString(),
    +            "RearDisplay Session has been started")
             updateRearDisplayButton()
         }
     
         override fun onSessionEnded(t: Throwable?) {
             rearDisplaySession = null
    -        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
    -        infoLogAdapter.notifyDataSetChanged()
    +        infoLogAdapter.appendAndNotify(getCurrentTimeString(), "RearDisplay Session has ended")
             updateRearDisplayButton()
         }
     
    @@ -133,22 +146,26 @@
                 binding.rearDisplayButton.text = "Disable RearDisplay Mode"
                 return
             }
    -        when (rearDisplayStatus) {
    -            WINDOW_AREA_STATUS_UNSUPPORTED -> {
    -                binding.rearDisplayButton.isEnabled = false
    -                binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
    -            }
    -            WINDOW_AREA_STATUS_UNAVAILABLE -> {
    -                binding.rearDisplayButton.isEnabled = false
    -                binding.rearDisplayButton.text = "RearDisplay is not currently available"
    -            }
    -            WINDOW_AREA_STATUS_AVAILABLE -> {
    -                binding.rearDisplayButton.isEnabled = true
    -                binding.rearDisplayButton.text = "Enable RearDisplay Mode"
    -            }
    -            WINDOW_AREA_STATUS_ACTIVE -> {
    -                binding.rearDisplayButton.isEnabled = true
    -                binding.rearDisplayButton.text = "Disable RearDisplay Mode"
    +        currentWindowAreaInfo?.let { windowAreaInfo ->
    +            when (windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA).status) {
    +                WINDOW_AREA_STATUS_UNSUPPORTED -> {
    +                    binding.rearDisplayButton.isEnabled = false
    +                    binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
    +                }
    +
    +                WINDOW_AREA_STATUS_UNAVAILABLE -> {
    +                    binding.rearDisplayButton.isEnabled = false
    +                    binding.rearDisplayButton.text = "RearDisplay is not currently available"
    +                }
    +
    +                WINDOW_AREA_STATUS_AVAILABLE -> {
    +                    binding.rearDisplayButton.isEnabled = true
    +                    binding.rearDisplayButton.text = "Enable RearDisplay Mode"
    +                }
    +                else -> {
    +                    binding.rearDisplayButton.isEnabled = false
    +                    binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
    +                }
                 }
             }
         }
    @@ -159,11 +176,6 @@
             return currentDate.toString()
         }
     
    -    private fun getRearDisplayStatus(windowAreaInfo: WindowAreaInfo?): WindowAreaCapability.Status {
    -        val status = windowAreaInfo?.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA)?.status
    -        return status ?: WINDOW_AREA_STATUS_UNSUPPORTED
    -    }
    -
         private companion object {
             private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
         }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt
    new file mode 100644
    index 0000000..5f5ba8b
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/area/RearDisplayPresentationActivity.kt
    
    @@ -0,0 +1,170 @@
    +/*
    + * 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 androidx.window.demo.area
    +
    +import android.content.Context
    +import android.os.Bundle
    +import android.view.LayoutInflater
    +import androidx.appcompat.app.AppCompatActivity
    +import androidx.lifecycle.Lifecycle
    +import androidx.lifecycle.lifecycleScope
    +import androidx.lifecycle.repeatOnLifecycle
    +import androidx.window.area.WindowAreaCapability
    +import androidx.window.area.WindowAreaCapability.Operation.Companion.OPERATION_PRESENT_ON_AREA
    +import androidx.window.area.WindowAreaController
    +import androidx.window.area.WindowAreaInfo
    +import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
    +import androidx.window.area.WindowAreaPresentationSessionCallback
    +import androidx.window.area.WindowAreaSessionPresenter
    +import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.demo.R
    +import androidx.window.demo.common.infolog.InfoLogAdapter
    +import androidx.window.demo.databinding.ActivityRearDisplayPresentationBinding
    +import java.text.SimpleDateFormat
    +import java.util.Date
    +import java.util.Locale
    +import kotlinx.coroutines.Dispatchers
    +import kotlinx.coroutines.launch
    +
    +/**
    + * Demo Activity that showcases listening for the status of the [OPERATION_PRESENT_ON_AREA]
    + * operation on a [WindowAreaInfo] of type [TYPE_REAR_FACING] as well as enabling/disabling a
    + * presentation session on that window area. This Activity implements
    + * [WindowAreaPresentationSessionCallback] for simplicity.
    + *
    + * This Activity overrides configuration changes for simplicity.
    + */
    +@OptIn(ExperimentalWindowApi::class)
    +class RearDisplayPresentationActivity : AppCompatActivity(),
    +    WindowAreaPresentationSessionCallback {
    +
    +    private var activePresentation: WindowAreaSessionPresenter? = null
    +    private var currentWindowAreaInfo: WindowAreaInfo? = null
    +    private lateinit var windowAreaController: WindowAreaController
    +    private val infoLogAdapter = InfoLogAdapter()
    +
    +    private lateinit var binding: ActivityRearDisplayPresentationBinding
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +
    +        windowAreaController = WindowAreaController.getOrCreate()
    +
    +        binding = ActivityRearDisplayPresentationBinding.inflate(layoutInflater)
    +        setContentView(binding.root)
    +        binding.rearStatusRecyclerView.adapter = infoLogAdapter
    +
    +        lifecycleScope.launch(Dispatchers.Main) {
    +            // The block passed to repeatOnLifecycle is executed when the lifecycle
    +            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
    +            // It automatically restarts the block when the lifecycle is STARTED again.
    +            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
    +                // Safely collect from windowInfoRepo when the lifecycle is STARTED
    +                // and stops collection when the lifecycle is STOPPED
    +                windowAreaController.windowAreaInfos.collect { windowAreaInfos ->
    +                    infoLogAdapter.appendAndNotify(
    +                        getCurrentTimeString(), "number of areas: " + windowAreaInfos.size
    +                    )
    +                    windowAreaInfos.forEach { windowAreaInfo ->
    +                        if (windowAreaInfo.type == TYPE_REAR_FACING) {
    +                            currentWindowAreaInfo = windowAreaInfo
    +                            val presentCapability = windowAreaInfo.getCapability(
    +                                OPERATION_PRESENT_ON_AREA
    +                            )
    +                            infoLogAdapter.append(
    +                                getCurrentTimeString(),
    +                                presentCapability.status.toString() + " : " +
    +                                    windowAreaInfo.metrics.toString()
    +                            )
    +                            updateRearDisplayPresentationButton()
    +                        }
    +                    }
    +                    infoLogAdapter.notifyDataSetChanged()
    +                }
    +            }
    +        }
    +
    +        binding.rearDisplayPresentationButton.setOnClickListener {
    +            if (activePresentation != null) {
    +                activePresentation?.close()
    +            } else {
    +                currentWindowAreaInfo?.let {
    +                    windowAreaController.presentContentOnWindowArea(
    +                        it.token,
    +                        this@RearDisplayPresentationActivity,
    +                        { obj: Runnable -> obj.run() },
    +                        this@RearDisplayPresentationActivity
    +                    )
    +                }
    +            }
    +        }
    +    }
    +
    +    override fun onSessionStarted(session: WindowAreaSessionPresenter) {
    +        infoLogAdapter.appendAndNotify(getCurrentTimeString(),
    +            "Presentation session has been started")
    +
    +        activePresentation = session
    +        val concurrentContext: Context = session.context
    +        val contentView = LayoutInflater.from(concurrentContext).inflate(
    +            R.layout.concurrent_presentation, null)
    +        session.setContentView(contentView)
    +        activePresentation = session
    +        updateRearDisplayPresentationButton()
    +    }
    +
    +    override fun onContainerVisibilityChanged(isVisible: Boolean) {
    +        infoLogAdapter.appendAndNotify(getCurrentTimeString(),
    +            "Presentation content is visible: $isVisible")
    +    }
    +
    +    override fun onSessionEnded(t: Throwable?) {
    +        infoLogAdapter.appendAndNotify(getCurrentTimeString(),
    +            "Presentation session has been ended")
    +        activePresentation = null
    +    }
    +
    +    private fun updateRearDisplayPresentationButton() {
    +        if (activePresentation != null) {
    +            binding.rearDisplayPresentationButton.isEnabled = true
    +            binding.rearDisplayPresentationButton.text = "Disable rear display presentation"
    +            return
    +        }
    +        when (currentWindowAreaInfo?.getCapability(OPERATION_PRESENT_ON_AREA)?.status) {
    +            WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
    +                binding.rearDisplayPresentationButton.isEnabled = false
    +                binding.rearDisplayPresentationButton.text =
    +                    "Rear display presentation mode is not supported on this device"
    +            }
    +            WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
    +                binding.rearDisplayPresentationButton.isEnabled = false
    +                binding.rearDisplayPresentationButton.text =
    +                    "Rear display presentation is not currently available"
    +            }
    +            WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
    +                binding.rearDisplayPresentationButton.isEnabled = true
    +                binding.rearDisplayPresentationButton.text = "Enable rear display presentation mode"
    +            }
    +        }
    +    }
    +
    +    private fun getCurrentTimeString(): String {
    +        val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
    +        val currentDate = sdf.format(Date())
    +        return currentDate.toString()
    +    }
    +}
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
    index e3d2706..addc7ce 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
    
    @@ -35,6 +35,7 @@
     import androidx.window.demo.R.string.show_all_display_features_portrait_slim
     import androidx.window.demo.SplitLayoutActivity
     import androidx.window.demo.WindowMetricsActivity
    +import androidx.window.demo.area.RearDisplayPresentationActivity
     import androidx.window.demo.common.DisplayFeaturesActivity
     
     /**
    @@ -85,6 +86,11 @@
                     buttonTitle = getString(R.string.ime),
                     description = getString(R.string.ime_demo_description),
                     clazz = ImeActivity::class.java
    +            ),
    +            DemoItem(
    +                buttonTitle = getString(R.string.dual_display),
    +                description = getString(R.string.dual_display_description),
    +                clazz = RearDisplayPresentationActivity::class.java
                 )
             )
             val recyclerView = findViewById(R.id.demo_recycler_view)
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
    index d06ea28..3d6528f 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
    
    @@ -16,9 +16,14 @@
     
     package androidx.window.demo.embedding
     
    +import android.graphics.Color
     import androidx.annotation.GuardedBy
    +import androidx.window.demo.embedding.OverlayActivityBase.Companion.DEFAULT_OVERLAY_ATTRIBUTES
    +import androidx.window.embedding.EmbeddingAnimationBackground
    +import androidx.window.embedding.OverlayAttributes
     import androidx.window.embedding.SplitAttributes
     import java.util.concurrent.atomic.AtomicBoolean
    +import java.util.concurrent.atomic.AtomicInteger
     import java.util.concurrent.locks.ReentrantLock
     import kotlin.concurrent.withLock
     
    @@ -61,6 +66,38 @@
         @GuardedBy("lock")
         private var splitTypeLocked = SplitAttributes.SplitType.SPLIT_TYPE_EQUAL
     
    +    @GuardedBy("lock")
    +    private var animationBackgroundLocked = EmbeddingAnimationBackground.DEFAULT
    +
    +    internal var animationBackground: EmbeddingAnimationBackground
    +        get() {
    +            lock.withLock {
    +                return animationBackgroundLocked
    +            }
    +        }
    +        set(value) {
    +            lock.withLock {
    +                animationBackgroundLocked = value
    +            }
    +        }
    +
    +    internal var overlayAttributes: OverlayAttributes
    +        get() {
    +            lock.withLock {
    +                return overlayAttributesLocked
    +            }
    +        }
    +        set(value) {
    +            lock.withLock {
    +                overlayAttributesLocked = value
    +            }
    +        }
    +
    +    @GuardedBy("lock")
    +    private var overlayAttributesLocked = DEFAULT_OVERLAY_ATTRIBUTES
    +
    +    internal var overlayMode = AtomicInteger()
    +
         companion object {
             @Volatile
             private var globalInstance: DemoActivityEmbeddingController? = null
    @@ -80,5 +117,14 @@
                 }
                 return globalInstance!!
             }
    +
    +        /** Anmiation background constants. */
    +        val ANIMATION_BACKGROUND_TEXTS = arrayOf("DEFAULT", "BLUE", "GREEN", "YELLOW")
    +        val ANIMATION_BACKGROUND_VALUES = arrayOf(
    +            EmbeddingAnimationBackground.DEFAULT,
    +            EmbeddingAnimationBackground.createColorBackground(Color.BLUE),
    +            EmbeddingAnimationBackground.createColorBackground(Color.GREEN),
    +            EmbeddingAnimationBackground.createColorBackground(Color.YELLOW)
    +        )
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt
    new file mode 100644
    index 0000000..45f0eb5
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DialogActivity.kt
    
    @@ -0,0 +1,22 @@
    +/*
    + * 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 androidx.window.demo.embedding
    +
    +import androidx.appcompat.app.AppCompatActivity
    +
    +/** Dialog style Activity. */
    +open class DialogActivity : AppCompatActivity()
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
    index 55425fa..b8459ab 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
    
    @@ -19,7 +19,11 @@
     import android.content.Context
     import androidx.startup.Initializer
     import androidx.window.WindowSdkExtensions
    +import androidx.window.core.ExperimentalWindowApi
     import androidx.window.demo.R
    +import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CHANGE_WITH_ORIENTATION
    +import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CUSTOMIZATION
    +import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_SIMPLE
     import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_FULLSCREEN_TOGGLE
     import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.PREFIX_PLACEHOLDER
     import androidx.window.demo.embedding.SplitAttributesToggleMainActivity.Companion.TAG_CUSTOMIZED_SPLIT_ATTRIBUTES
    @@ -31,6 +35,13 @@
     import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP
     import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING
     import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_USE_DEFAULT_SPLIT_ATTRIBUTES
    +import androidx.window.embedding.ActivityEmbeddingController
    +import androidx.window.embedding.EmbeddingBounds
    +import androidx.window.embedding.EmbeddingConfiguration
    +import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion.ON_TASK
    +import androidx.window.embedding.OverlayAttributes
    +import androidx.window.embedding.OverlayAttributesCalculatorParams
    +import androidx.window.embedding.OverlayController
     import androidx.window.embedding.RuleController
     import androidx.window.embedding.SplitAttributes
     import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
    @@ -52,16 +63,33 @@
      */
     class ExampleWindowInitializer : Initializer {
     
    -    private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
    +    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
     
    +    private lateinit var splitController: SplitController
    +
    +    private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
    +
    +    @OptIn(ExperimentalWindowApi::class)
         override fun create(context: Context): RuleController {
    -        SplitController.getInstance(context).apply {
    -            if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    -                setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
    +        splitController = SplitController.getInstance(context)
    +
    +        if (extensionVersion >= 2) {
    +            splitController.setSplitAttributesCalculator(::sampleSplitAttributesCalculator)
    +        }
    +        if (extensionVersion >= 6) {
    +            OverlayController.getInstance(context).setOverlayAttributesCalculator(
    +                ::sampleOverlayAttributesCalculator
    +            )
    +        }
    +        ActivityEmbeddingController.getInstance(context).apply {
    +            if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
    +                setEmbeddingConfiguration(
    +                    EmbeddingConfiguration.Builder().setDimAreaBehavior(ON_TASK).build()
    +                )
                 }
             }
             return RuleController.getInstance(context).apply {
    -            if (SplitController.getInstance(context).splitSupportStatus == SPLIT_AVAILABLE) {
    +            if (splitController.splitSupportStatus == SPLIT_AVAILABLE) {
                     setRules(RuleController.parseRules(context, R.xml.main_split_config))
                 }
             }
    @@ -81,7 +109,7 @@
                 .setSplitType(SPLIT_TYPE_EXPAND)
                 .build()
             if (tag?.startsWith(PREFIX_FULLSCREEN_TOGGLE) == true &&
    -            mDemoActivityEmbeddingController.shouldExpandSecondaryContainer.get()
    +            demoActivityEmbeddingController.shouldExpandSecondaryContainer.get()
             ) {
                 return expandContainersAttrs
             }
    @@ -92,10 +120,17 @@
             val config = params.parentConfiguration
             val shouldReversed = tag?.contains(SUFFIX_REVERSED) ?: false
             // Make a copy of the default splitAttributes, but replace the animation background
    -        // color to what is configured in the Demo app.
    +        // to what is configured in the Demo app.
    +        val animationBackground = demoActivityEmbeddingController.animationBackground
             val defaultSplitAttributes = SplitAttributes.Builder()
                 .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
                 .setSplitType(params.defaultSplitAttributes.splitType)
    +            .setAnimationBackground(animationBackground)
    +            .apply {
    +                if (extensionVersion >= 6) {
    +                    setDividerAttributes(params.defaultSplitAttributes.dividerAttributes)
    +                }
    +            }
                 .build()
             when (tag
                 ?.removePrefix(PREFIX_FULLSCREEN_TOGGLE)
    @@ -125,6 +160,7 @@
                                     TOP_TO_BOTTOM
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     } else if (isPortrait) {
                         return expandContainersAttrs
    @@ -141,6 +177,7 @@
                                     TOP_TO_BOTTOM
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     }
                 }
    @@ -155,6 +192,7 @@
                                     TOP_TO_BOTTOM
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     } else {
                         SplitAttributes.Builder()
    @@ -166,6 +204,7 @@
                                     LEFT_TO_RIGHT
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     }
                 }
    @@ -182,6 +221,7 @@
                                     TOP_TO_BOTTOM
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     } else {
                         SplitAttributes.Builder()
    @@ -193,6 +233,7 @@
                                     LEFT_TO_RIGHT
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     }
                 }
    @@ -216,19 +257,49 @@
                                     if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
                                 }
                             )
    +                        .setAnimationBackground(animationBackground)
                             .build()
                     }
                 }
                 TAG_CUSTOMIZED_SPLIT_ATTRIBUTES -> {
                     return SplitAttributes.Builder()
    -                    .setSplitType(mDemoActivityEmbeddingController.customizedSplitType)
    -                    .setLayoutDirection(mDemoActivityEmbeddingController.customizedLayoutDirection)
    +                    .setSplitType(demoActivityEmbeddingController.customizedSplitType)
    +                    .setLayoutDirection(demoActivityEmbeddingController.customizedLayoutDirection)
    +                    .setAnimationBackground(animationBackground)
                         .build()
                 }
             }
             return defaultSplitAttributes
         }
     
    +    private fun sampleOverlayAttributesCalculator(
    +        params: OverlayAttributesCalculatorParams
    +    ): OverlayAttributes = when (val mode = demoActivityEmbeddingController.overlayMode.get()) {
    +            // Put the overlay to the right
    +            OVERLAY_MODE_SIMPLE.value -> params.defaultOverlayAttributes
    +            // Update the overlay with orientation:
    +            // - Put the overlay to the bottom if the device is in portrait
    +            // - Otherwise, put the overlay to the right if the device is in landscape
    +            OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value -> OverlayAttributes(
    +                if (params.parentWindowMetrics.isPortrait()) {
    +                    EmbeddingBounds(
    +                        EmbeddingBounds.Alignment.ALIGN_BOTTOM,
    +                        width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
    +                        height = EmbeddingBounds.Dimension.ratio(0.4f)
    +                    )
    +                } else {
    +                    EmbeddingBounds(
    +                        EmbeddingBounds.Alignment.ALIGN_RIGHT,
    +                        width = EmbeddingBounds.Dimension.ratio(0.5f),
    +                        height = EmbeddingBounds.Dimension.ratio(0.8f)
    +                    )
    +                }
    +            )
    +            // Fully customized overlay presentation
    +            OVERLAY_MODE_CUSTOMIZATION.value -> demoActivityEmbeddingController.overlayAttributes
    +            else -> throw IllegalStateException("Unknown mode $mode")
    +        }
    +
         private fun WindowMetrics.isPortrait(): Boolean =
             bounds.height() > bounds.width()
     
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt
    new file mode 100644
    index 0000000..eaddcaf
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayActivityBase.kt
    
    @@ -0,0 +1,417 @@
    +/*
    + * 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 androidx.window.demo.embedding
    +
    +import android.app.ActivityOptions
    +import android.content.ActivityNotFoundException
    +import android.content.Intent
    +import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
    +import android.graphics.Color
    +import android.os.Bundle
    +import android.view.View
    +import android.widget.AdapterView
    +import android.widget.ArrayAdapter
    +import android.widget.RadioGroup
    +import android.widget.SeekBar
    +import android.widget.Spinner
    +import android.widget.Toast
    +import androidx.appcompat.app.AppCompatActivity
    +import androidx.lifecycle.Lifecycle
    +import androidx.lifecycle.lifecycleScope
    +import androidx.lifecycle.repeatOnLifecycle
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.demo.R
    +import androidx.window.demo.databinding.ActivityOverlayActivityLayoutBinding
    +import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CHANGE_WITH_ORIENTATION
    +import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_CUSTOMIZATION
    +import androidx.window.demo.embedding.OverlayActivityBase.OverlayMode.Companion.OVERLAY_MODE_SIMPLE
    +import androidx.window.embedding.ActivityEmbeddingController
    +import androidx.window.embedding.ActivityStack
    +import androidx.window.embedding.EmbeddingBounds
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP
    +import androidx.window.embedding.EmbeddingBounds.Dimension
    +import androidx.window.embedding.OverlayAttributes
    +import androidx.window.embedding.OverlayController
    +import androidx.window.embedding.OverlayCreateParams
    +import androidx.window.embedding.OverlayInfo
    +import androidx.window.embedding.SplitController
    +import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
    +import androidx.window.embedding.setLaunchingActivityStack
    +import androidx.window.embedding.setOverlayCreateParams
    +import kotlinx.coroutines.launch
    +
    +open class OverlayActivityBase : AppCompatActivity(), View.OnClickListener,
    +    RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener,
    +    SeekBar.OnSeekBarChangeListener {
    +
    +    private val overlayTag = OverlayCreateParams.generateOverlayTag()
    +
    +    private lateinit var splitController: SplitController
    +
    +    private lateinit var overlayController: OverlayController
    +
    +    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
    +
    +    private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
    +
    +    lateinit var viewBinding: ActivityOverlayActivityLayoutBinding
    +
    +    private var overlayActivityStack: ActivityStack? = null
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +
    +        viewBinding = ActivityOverlayActivityLayoutBinding.inflate(layoutInflater)
    +        splitController = SplitController.getInstance(this)
    +        overlayController = OverlayController.getInstance(this)
    +
    +        if (splitController.splitSupportStatus != SPLIT_AVAILABLE || extensionVersion < 6) {
    +            Toast.makeText(this, R.string.toast_show_overlay_warning,
    +                Toast.LENGTH_SHORT).show()
    +            finish()
    +            return
    +        }
    +
    +        viewBinding.root.setBackgroundColor(Color.parseColor("#fff3e0"))
    +        setContentView(viewBinding.root)
    +
    +        viewBinding.buttonUpdateOverlayLayout.setOnClickListener(this)
    +        viewBinding.buttonLaunchOverlayContainer.setOnClickListener(this)
    +        viewBinding.buttonLaunchOverlayActivityA.setOnClickListener(this)
    +        viewBinding.buttonLaunchOverlayActivityB.setOnClickListener(this)
    +        viewBinding.buttonFinishThisActivity.setOnClickListener(this)
    +
    +        val radioGroupChooseOverlayLayout = viewBinding.radioGroupChooseOverlayLayout
    +        radioGroupChooseOverlayLayout.setOnCheckedChangeListener(this)
    +
    +        viewBinding.spinnerAlignment.apply {
    +            adapter = ArrayAdapter(
    +                this@OverlayActivityBase,
    +                android.R.layout.simple_spinner_dropdown_item,
    +                POSITION_TEXT_ARRAY,
    +            )
    +            onItemSelectedListener = this@OverlayActivityBase
    +        }
    +
    +        val dimensionAdapter = ArrayAdapter(
    +            this,
    +            android.R.layout.simple_spinner_dropdown_item,
    +            DIMENSION_TYPE_TEXT_ARRAY,
    +        )
    +        viewBinding.spinnerWidth.apply {
    +            adapter = dimensionAdapter
    +            onItemSelectedListener = this@OverlayActivityBase
    +        }
    +
    +        viewBinding.spinnerHeight.apply {
    +            adapter = dimensionAdapter
    +            onItemSelectedListener = this@OverlayActivityBase
    +        }
    +
    +        viewBinding.seekBarHeightInRatio.setOnSeekBarChangeListener(this)
    +        viewBinding.seekBarWidthInRatio.setOnSeekBarChangeListener(this)
    +
    +        initializeUi()
    +
    +        lifecycleScope.launch {
    +            // The block passed to repeatOnLifecycle is executed when the lifecycle
    +            // is at least STARTED and is cancelled when the lifecycle is STOPPED.
    +            // It automatically restarts the block when the lifecycle is STARTED again.
    +            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
    +                overlayController.overlayInfo(overlayTag)
    +                    .collect { overlayInfo ->
    +                        overlayActivityStack = overlayInfo.activityStack
    +                        val hasOverlay = overlayActivityStack != null
    +                        viewBinding.buttonUpdateOverlayLayout.isEnabled =
    +                            hasOverlay && demoActivityEmbeddingController.overlayMode.get() !=
    +                                OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value
    +                        updateOverlayBoundsText(overlayInfo)
    +                    }
    +            }
    +        }
    +    }
    +
    +    private fun initializeUi() {
    +        viewBinding.buttonUpdateOverlayLayout.isEnabled = false
    +        viewBinding.radioGroupChooseOverlayLayout.check(R.id.radioButton_simple_overlay)
    +        viewBinding.spinnerAlignment.setSelection(ALIGNMENT_VALUE_ARRAY.indexOf(ALIGN_RIGHT))
    +        viewBinding.spinnerWidth.apply {
    +            setSelection(INDEX_DIMENSION_RATIO)
    +            updateDimensionUi(this)
    +        }
    +        viewBinding.spinnerHeight.apply {
    +            setSelection(INDEX_DIMENSION_RATIO)
    +            updateDimensionUi(this)
    +        }
    +    }
    +
    +    private fun initializeSeekbar(seekBar: SeekBar) {
    +        seekBar.progress = 50
    +        updateRatioText(seekBar)
    +    }
    +
    +    private fun updateOverlayBoundsText(overlayInfo: OverlayInfo) {
    +        viewBinding.textViewOverlayBounds.text = resources.getString(R.string.overlay_bounds_text) +
    +            overlayInfo.currentOverlayAttributes?.bounds.toString()
    +    }
    +
    +    @OptIn(ExperimentalWindowApi::class)
    +    override fun onClick(button: View) {
    +        val overlayAttributes = buildOverlayAttributesFromUi()
    +        val isCustomizationMode =
    +            demoActivityEmbeddingController.overlayMode.get() == OVERLAY_MODE_CUSTOMIZATION.value
    +        when (button.id) {
    +            R.id.button_launch_overlay_container -> {
    +                if (isCustomizationMode) {
    +                    // Also update controller's overlayAttributes because the launch bounds are
    +                    // determined by calculator, which returns the overlayAttributes from
    +                    // the controller directly.
    +                    demoActivityEmbeddingController.overlayAttributes = overlayAttributes
    +                }
    +                try {
    +                    startActivity(
    +                        Intent().apply {
    +                            setClassName(
    +                                "androidx.window.demo2",
    +                                "androidx.window.demo2.embedding.UntrustedEmbeddingActivity"
    +                            )
    +                        },
    +                        ActivityOptions.makeBasic().toBundle().setOverlayCreateParams(
    +                            this,
    +                            OverlayCreateParams.Builder()
    +                                .setTag(overlayTag)
    +                                .setOverlayAttributes(
    +                                    if (isCustomizationMode) {
    +                                        overlayAttributes
    +                                    } else {
    +                                        DEFAULT_OVERLAY_ATTRIBUTES
    +                                    }
    +                                ).build()
    +                        )
    +                    )
    +                } catch (e: ActivityNotFoundException) {
    +                    Toast.makeText(this, R.string.install_samples_2, Toast.LENGTH_LONG).show()
    +                }
    +            }
    +            R.id.button_launch_overlay_activity_a -> startActivity(
    +                Intent(this, OverlayAssociatedActivityA::class.java).apply {
    +                    if (viewBinding.checkboxReorderToFront.isChecked) {
    +                        flags = FLAG_ACTIVITY_REORDER_TO_FRONT
    +                    }
    +                }
    +            )
    +            R.id.button_launch_overlay_activity_b -> startActivity(
    +                Intent(this, OverlayAssociatedActivityB::class.java),
    +                overlayActivityStack?.let {
    +                    if (viewBinding.checkboxLaunchToOverlay.isChecked) {
    +                        ActivityOptions.makeBasic().toBundle().setLaunchingActivityStack(
    +                            this,
    +                            it,
    +                        )
    +                    } else {
    +                        null
    +                    }
    +                }
    +            )
    +            R.id.button_finish_this_activity -> finish()
    +            R.id.button_update_overlay_layout -> {
    +                if (isCustomizationMode) {
    +                    demoActivityEmbeddingController.overlayAttributes = overlayAttributes
    +                    ActivityEmbeddingController
    +                        .getInstance(this).invalidateVisibleActivityStacks()
    +                } else {
    +                    overlayController.updateOverlayAttributes(overlayTag, overlayAttributes)
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun buildOverlayAttributesFromUi(): OverlayAttributes {
    +        val spinnerPosition = viewBinding.spinnerAlignment
    +        val spinnerWidth = viewBinding.spinnerWidth
    +        val spinnerHeight = viewBinding.spinnerHeight
    +
    +        return OverlayAttributes.Builder()
    +            .setBounds(
    +                EmbeddingBounds(
    +                    ALIGNMENT_VALUE_ARRAY[spinnerPosition.selectedItemPosition],
    +                    createDimensionFromUi(spinnerWidth),
    +                    createDimensionFromUi(spinnerHeight),
    +                )
    +            ).build()
    +    }
    +
    +    private fun createDimensionFromUi(spinner: Spinner): Dimension =
    +        when (val position = spinner.selectedItemPosition) {
    +            INDEX_DIMENSION_EXPAND -> Dimension.DIMENSION_EXPANDED
    +            INDEX_DIMENSION_HINGE -> Dimension.DIMENSION_HINGE
    +            INDEX_DIMENSION_RATIO -> Dimension.ratio(
    +                if (spinner.isSpinnerWidth()) {
    +                    viewBinding.seekBarWidthInRatio.progress.toFloat() / 100
    +                } else {
    +                    viewBinding.seekBarHeightInRatio.progress.toFloat() / 100
    +                }
    +            )
    +            INDEX_DIMENSION_PIXEL -> Dimension.pixel(
    +                if (spinner.isSpinnerWidth()) {
    +                    viewBinding.editTextNumberDecimalWidthInPixel.text.toString().toInt()
    +                } else {
    +                    viewBinding.editTextNumberDecimalHeightInPixel.text.toString().toInt()
    +                }
    +            )
    +            else -> throw IllegalStateException("Unknown spinner index: $position")
    +        }
    +
    +    override fun onCheckedChanged(group: RadioGroup, id: Int) {
    +        demoActivityEmbeddingController.overlayMode.set(
    +            when (id) {
    +                R.id.radioButton_simple_overlay -> OVERLAY_MODE_SIMPLE.value
    +                R.id.radioButton_change_with_orientation ->
    +                    OVERLAY_MODE_CHANGE_WITH_ORIENTATION.value
    +                R.id.radioButton_customization -> OVERLAY_MODE_CUSTOMIZATION.value
    +                else -> throw IllegalArgumentException("Unrecognized id $id")
    +            }
    +        )
    +    }
    +
    +    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
    +        if (parent is Spinner &&
    +            parent in arrayOf(viewBinding.spinnerWidth, viewBinding.spinnerHeight)
    +        ) {
    +            updateDimensionUi(parent)
    +        }
    +    }
    +
    +    private fun updateDimensionUi(spinner: Spinner) {
    +        val textViewRatio = if (spinner.isSpinnerWidth()) {
    +            viewBinding.textViewWidthInRatio
    +        } else {
    +            viewBinding.textViewHeightInRatio
    +        }
    +        val seekBarRatio = if (spinner.isSpinnerWidth()) {
    +            viewBinding.seekBarWidthInRatio
    +        } else {
    +            viewBinding.seekBarHeightInRatio
    +        }
    +        val textViewPixel = if (spinner.isSpinnerWidth()) {
    +            viewBinding.textViewWidthInPixel
    +        } else {
    +            viewBinding.textViewHeightInPixel
    +        }
    +        val editTextPixel = if (spinner.isSpinnerWidth()) {
    +            viewBinding.editTextNumberDecimalWidthInPixel
    +        } else {
    +            viewBinding.editTextNumberDecimalHeightInPixel
    +        }
    +        when (spinner.selectedItemPosition) {
    +            INDEX_DIMENSION_EXPAND, INDEX_DIMENSION_HINGE -> {
    +                textViewRatio.visibility = View.GONE
    +                seekBarRatio.visibility = View.GONE
    +                textViewPixel.visibility = View.GONE
    +                editTextPixel.visibility = View.GONE
    +            }
    +            INDEX_DIMENSION_RATIO -> {
    +                textViewRatio.visibility = View.VISIBLE
    +                seekBarRatio.visibility = View.VISIBLE
    +                textViewPixel.visibility = View.GONE
    +                editTextPixel.visibility = View.GONE
    +                initializeSeekbar(seekBarRatio)
    +            }
    +            INDEX_DIMENSION_PIXEL -> {
    +                textViewRatio.visibility = View.GONE
    +                seekBarRatio.visibility = View.GONE
    +                textViewPixel.visibility = View.VISIBLE
    +                editTextPixel.visibility = View.VISIBLE
    +                editTextPixel.text.clear()
    +            }
    +        }
    +    }
    +
    +    private fun Spinner.isSpinnerWidth() = this == viewBinding.spinnerWidth
    +
    +    override fun onNothingSelected(view: AdapterView<*>?) {
    +        // Auto-generated method stub
    +    }
    +
    +    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
    +        updateRatioText(seekBar)
    +    }
    +
    +    private fun updateRatioText(seekBar: SeekBar) {
    +        if (seekBar.isSeekBarWidthInRatio()) {
    +            viewBinding.textViewWidthInRatio.text =
    +                resources.getString(R.string.width_in_ratio) +
    +                    (seekBar.progress.toFloat() / 100).toString()
    +        } else {
    +            viewBinding.textViewHeightInRatio.text =
    +                resources.getString(R.string.height_in_ratio) +
    +                    (seekBar.progress.toFloat() / 100).toString()
    +        }
    +    }
    +
    +    private fun SeekBar.isSeekBarWidthInRatio(): Boolean = this == viewBinding.seekBarWidthInRatio
    +
    +    override fun onStartTrackingTouch(seekBar: SeekBar) {
    +        // Auto-generated method stub
    +    }
    +
    +    override fun onStopTrackingTouch(seekBar: SeekBar) {
    +        // Auto-generated method stub
    +    }
    +
    +    @JvmInline
    +    internal value class OverlayMode(val value: Int) {
    +        companion object {
    +            val OVERLAY_MODE_SIMPLE = OverlayMode(0)
    +            val OVERLAY_MODE_CHANGE_WITH_ORIENTATION = OverlayMode(1)
    +            val OVERLAY_MODE_CUSTOMIZATION = OverlayMode(2)
    +        }
    +    }
    +
    +    companion object {
    +        internal val DEFAULT_OVERLAY_ATTRIBUTES = OverlayAttributes(
    +            EmbeddingBounds(
    +                ALIGN_RIGHT,
    +                Dimension.ratio(0.5f),
    +                Dimension.ratio(0.8f),
    +            )
    +        )
    +
    +        private val POSITION_TEXT_ARRAY = arrayOf("top", "left", "bottom", "right")
    +        private val ALIGNMENT_VALUE_ARRAY = arrayListOf(
    +            ALIGN_TOP,
    +            ALIGN_LEFT,
    +            ALIGN_BOTTOM,
    +            ALIGN_RIGHT,
    +        )
    +
    +        private val DIMENSION_TYPE_TEXT_ARRAY = arrayOf(
    +            "expand to the task",
    +            "follow the hinge",
    +            "dimension in ratio",
    +            "dimension in pixel",
    +        )
    +        private const val INDEX_DIMENSION_EXPAND = 0
    +        private const val INDEX_DIMENSION_HINGE = 1
    +        private const val INDEX_DIMENSION_RATIO = 2
    +        private const val INDEX_DIMENSION_PIXEL = 3
    +    }
    +}
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt
    new file mode 100644
    index 0000000..0e35bb5
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityA.kt
    
    @@ -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 androidx.window.demo.embedding
    +
    +class OverlayAssociatedActivityA : OverlayActivityBase()
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt
    new file mode 100644
    index 0000000..edc9b7e
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/OverlayAssociatedActivityB.kt
    
    @@ -0,0 +1,28 @@
    +/*
    + * 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 androidx.window.demo.embedding
    +
    +import android.graphics.Color
    +import android.os.Bundle
    +
    +class OverlayAssociatedActivityB : OverlayActivityBase() {
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +
    +        viewBinding.rootOverlayActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
    +    }
    +}
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
    index aec91b9..4520b2c 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
    
    @@ -18,6 +18,7 @@
     
     import android.content.Intent
     import android.graphics.Color
    +import android.graphics.drawable.ColorDrawable
     import android.os.Bundle
     import android.view.View
     import androidx.window.demo.R
    @@ -25,8 +26,9 @@
     open class SplitActivityB : SplitActivityBase() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        findViewById(R.id.root_split_activity_layout)
    -            .setBackgroundColor(Color.parseColor("#fff3e0"))
    +        val color = Color.parseColor("#fff3e0")
    +        findViewById(R.id.root_split_activity_layout).setBackgroundColor(color)
    +        window.setBackgroundDrawable(ColorDrawable(color))
     
             if (intent.getBooleanExtra(EXTRA_LAUNCH_C_TO_SIDE, false)) {
                 startActivity(Intent(this, SplitActivityC::class.java))
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
    index 7ff6884..e4e1fd3 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
    
    @@ -18,12 +18,15 @@
     
     import static android.app.PendingIntent.FLAG_IMMUTABLE;
     
    -import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_AVAILABLE;
    +import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_ERROR_PROPERTY_NOT_DECLARED;
    +import static androidx.window.embedding.SplitController.SplitSupportStatus.SPLIT_UNAVAILABLE;
     import static androidx.window.embedding.SplitRule.FinishBehavior.ADJACENT;
     import static androidx.window.embedding.SplitRule.FinishBehavior.ALWAYS;
     import static androidx.window.embedding.SplitRule.FinishBehavior.NEVER;
     
     import android.app.Activity;
    +import android.app.ActivityOptions;
    +import android.app.AlertDialog;
     import android.app.PendingIntent;
     import android.content.ActivityNotFoundException;
     import android.content.ComponentName;
    @@ -42,8 +45,13 @@
     import androidx.window.demo.R;
     import androidx.window.demo.databinding.ActivitySplitActivityLayoutBinding;
     import androidx.window.embedding.ActivityEmbeddingController;
    +import androidx.window.embedding.ActivityEmbeddingOptions;
     import androidx.window.embedding.ActivityFilter;
     import androidx.window.embedding.ActivityRule;
    +import androidx.window.embedding.DividerAttributes;
    +import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes;
    +import androidx.window.embedding.DividerAttributes.FixedDividerAttributes;
    +import androidx.window.embedding.EmbeddedActivityWindowInfo;
     import androidx.window.embedding.EmbeddingRule;
     import androidx.window.embedding.RuleController;
     import androidx.window.embedding.SplitAttributes;
    @@ -51,7 +59,9 @@
     import androidx.window.embedding.SplitInfo;
     import androidx.window.embedding.SplitPairFilter;
     import androidx.window.embedding.SplitPairRule;
    +import androidx.window.embedding.SplitPinRule;
     import androidx.window.embedding.SplitPlaceholderRule;
    +import androidx.window.java.embedding.ActivityEmbeddingControllerCallbackAdapter;
     import androidx.window.java.embedding.SplitControllerCallbackAdapter;
     
     import java.util.HashSet;
    @@ -76,7 +86,11 @@
          */
         private SplitControllerCallbackAdapter mSplitControllerAdapter;
         private RuleController mRuleController;
    -    private SplitInfoCallback mCallback;
    +    private SplitInfoCallback mSplitInfoCallback;
    +
    +    private ActivityEmbeddingController mActivityEmbeddingController;
    +    private ActivityEmbeddingControllerCallbackAdapter mActivityEmbeddingControllerCallbackAdapter;
    +    private EmbeddedActivityWindowInfoCallback mEmbeddedActivityWindowInfoCallbackCallback;
     
         private ActivitySplitActivityLayoutBinding mViewBinding;
     
    @@ -86,9 +100,28 @@
         @Override
         protected void onCreate(@Nullable Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
    +
    +        final SplitController splitController = SplitController.getInstance(this);
    +        final SplitController.SplitSupportStatus splitSupportStatus =
    +                splitController.getSplitSupportStatus();
    +        if (splitSupportStatus == SPLIT_UNAVAILABLE) {
    +            Toast.makeText(this, R.string.toast_split_not_available,
    +                    Toast.LENGTH_SHORT).show();
    +            finish();
    +            return;
    +        } else if (splitSupportStatus == SPLIT_ERROR_PROPERTY_NOT_DECLARED) {
    +            Toast.makeText(this, R.string.toast_split_not_support,
    +                    Toast.LENGTH_SHORT).show();
    +            finish();
    +            return;
    +        }
    +
             mViewBinding = ActivitySplitActivityLayoutBinding.inflate(getLayoutInflater());
             setContentView(mViewBinding.getRoot());
     
    +        final int extensionVersion = WindowSdkExtensions.getInstance().getExtensionVersion();
    +        mActivityEmbeddingController = ActivityEmbeddingController.getInstance(this);
    +
             // Setup activity launch buttons and config options.
             mViewBinding.launchB.setOnClickListener((View v) ->
                     startActivity(new Intent(this, SplitActivityB.class)));
    @@ -99,10 +132,19 @@
             });
             mViewBinding.launchE.setOnClickListener((View v) -> {
                 Bundle bundle = null;
    +            if (mViewBinding.setLaunchingEInActivityStack.isChecked()) {
    +                try {
    +                    bundle = ActivityEmbeddingOptions.setLaunchingActivityStack(
    +                            ActivityOptions.makeBasic().toBundle(), this,
    +                            mActivityEmbeddingController.getActivityStack(this));
    +                } catch (UnsupportedOperationException ex) {
    +                    Log.w(TAG, "#setLaunchingActivityStack is not supported", ex);
    +                }
    +            }
                 startActivity(new Intent(this, SplitActivityE.class), bundle);
             });
    -        if (WindowSdkExtensions.getInstance().getExtensionVersion() < 3) {
    -            mViewBinding.setLaunchingEInActivityStack.setEnabled(false);
    +        if (extensionVersion < 3) {
    +            mViewBinding.setLaunchingEInActivityStack.setVisibility(View.GONE);
             }
             mViewBinding.launchF.setOnClickListener((View v) ->
                     startActivity(new Intent(this, SplitActivityF.class)));
    @@ -158,6 +200,35 @@
             });
             mViewBinding.launchExpandedDialogButton.setOnClickListener((View v) ->
                     startActivity(new Intent(this, ExpandedDialogActivity.class)));
    +        mViewBinding.launchDialogActivityButton.setOnClickListener((View v) ->
    +                startActivity(new Intent(this, DialogActivity.class)));
    +        mViewBinding.launchDialogButton.setOnClickListener((View v) ->
    +                new AlertDialog.Builder(this)
    +                        .setTitle("Alert dialog demo")
    +                        .setMessage("This is a dialog demo").create().show());
    +
    +        if (extensionVersion < 5) {
    +            mViewBinding.pinTopActivityStackButton.setVisibility(View.GONE);
    +            mViewBinding.unpinTopActivityStackButton.setVisibility(View.GONE);
    +        } else {
    +            mViewBinding.pinTopActivityStackButton.setOnClickListener((View v) -> {
    +                        splitController.pinTopActivityStack(getTaskId(),
    +                                new SplitPinRule.Builder().setSticky(
    +                                        mViewBinding.stickyPinRule.isChecked()).build());
    +                    }
    +            );
    +            mViewBinding.unpinTopActivityStackButton.setOnClickListener((View v) -> {
    +                        splitController.unpinTopActivityStack(getTaskId());
    +                    }
    +            );
    +        }
    +        if (extensionVersion < 6) {
    +            mViewBinding.dividerCheckBox.setVisibility(View.GONE);
    +            mViewBinding.draggableDividerCheckBox.setVisibility(View.GONE);
    +        } else {
    +            mViewBinding.dividerCheckBox.setOnCheckedChangeListener(this);
    +            mViewBinding.draggableDividerCheckBox.setOnCheckedChangeListener(this);
    +        }
     
             // Listen for split configuration checkboxes to update the rules before launching
             // activities.
    @@ -169,42 +240,84 @@
             mViewBinding.fullscreenECheckBox.setOnCheckedChangeListener(this);
             mViewBinding.splitWithFCheckBox.setOnCheckedChangeListener(this);
     
    -        final SplitController splitController = SplitController.getInstance(this);
    +        if (extensionVersion < 6) {
    +            mViewBinding.buttonLaunchOverlayAssociatedActivity.setVisibility(View.GONE);
    +        } else {
    +            mViewBinding.buttonLaunchOverlayAssociatedActivity.setOnClickListener((View v) ->
    +                    startActivity(new Intent(this, OverlayAssociatedActivityA.class)));
    +        }
    +
             mSplitControllerAdapter = new SplitControllerCallbackAdapter(splitController);
    -        if (splitController.getSplitSupportStatus() != SPLIT_AVAILABLE) {
    -            Toast.makeText(this, R.string.toast_split_not_support,
    -                    Toast.LENGTH_SHORT).show();
    -            finish();
    -            return;
    +        if (extensionVersion >= 6) {
    +            mActivityEmbeddingControllerCallbackAdapter =
    +                    new ActivityEmbeddingControllerCallbackAdapter(mActivityEmbeddingController);
    +
    +            // The EmbeddedActivityWindowInfoListener will only be triggered when the activity is
    +            // embedded and visible (just like Activity#onConfigurationChanged).
    +            // Register it in #onCreate instead of #onStart so that when the embedded status is
    +            // changed to non-embedded before #onStart (like screen rotation when this activity is
    +            // in background), the listener will be triggered right after #onStart.
    +            // Otherwise, if registered in #onStart, it will not be triggered on registration
    +            // because the activity is not embedded, which results it shows the stale info.
    +            mEmbeddedActivityWindowInfoCallbackCallback = new EmbeddedActivityWindowInfoCallback();
    +            mActivityEmbeddingControllerCallbackAdapter.addEmbeddedActivityWindowInfoListener(
    +                    this, Runnable::run, mEmbeddedActivityWindowInfoCallbackCallback);
             }
             mRuleController = RuleController.getInstance(this);
         }
     
         @Override
    +    protected void onDestroy() {
    +        super.onDestroy();
    +        if (mActivityEmbeddingControllerCallbackAdapter != null) {
    +            mActivityEmbeddingControllerCallbackAdapter.removeEmbeddedActivityWindowInfoListener(
    +                    mEmbeddedActivityWindowInfoCallbackCallback);
    +            mEmbeddedActivityWindowInfoCallbackCallback = null;
    +        }
    +    }
    +
    +    @Override
         protected void onStart() {
             super.onStart();
    -        mCallback = new SplitInfoCallback();
    -        mSplitControllerAdapter.addSplitListener(this, Runnable::run, mCallback);
    +        mSplitInfoCallback = new SplitInfoCallback();
    +        mSplitControllerAdapter.addSplitListener(this, Runnable::run, mSplitInfoCallback);
         }
     
         @Override
         protected void onStop() {
             super.onStop();
    -        mSplitControllerAdapter.removeSplitListener(mCallback);
    -        mCallback = null;
    +        mSplitControllerAdapter.removeSplitListener(mSplitInfoCallback);
    +        mSplitInfoCallback = null;
         }
     
         /** Updates the embedding status when receives callback from the extension. */
    -    class SplitInfoCallback implements Consumer> {
    +    private class SplitInfoCallback implements Consumer> {
             @Override
             public void accept(List splitInfoList) {
                 runOnUiThread(() -> {
    -                updateEmbeddedStatus();
    +                if (mActivityEmbeddingControllerCallbackAdapter == null) {
    +                    // Otherwise, the embedded status will be updated from
    +                    // EmbeddedActivityWindowInfoCallback.
    +                    updateEmbeddedStatus(mActivityEmbeddingController.isActivityEmbedded(
    +                            SplitActivityBase.this));
    +                }
                     updateCheckboxesFromCurrentConfig();
                 });
             }
         }
     
    +    /** Updates the embedding status when receives callback from the extension. */
    +    private class EmbeddedActivityWindowInfoCallback implements
    +            Consumer {
    +        @Override
    +        public void accept(EmbeddedActivityWindowInfo embeddedActivityWindowInfo) {
    +            runOnUiThread(() -> {
    +                updateEmbeddedStatus(embeddedActivityWindowInfo.isEmbedded());
    +                updateEmbeddedWindowInfo(embeddedActivityWindowInfo);
    +            });
    +        }
    +    }
    +
         /** Called on checkbox changed. */
         @Override
         public void onCheckedChanged(@NonNull CompoundButton c, boolean isChecked) {
    @@ -328,8 +441,19 @@
         /** Updates the split rules based on the current selection on checkboxes. */
         private void updateRulesFromCheckboxes() {
             mRuleController.clearRules();
    +
    +        final DividerAttributes dividerAttributes;
    +        if (mViewBinding.dividerCheckBox.isChecked()) {
    +            dividerAttributes = mViewBinding.draggableDividerCheckBox.isChecked()
    +                    ? new DraggableDividerAttributes.Builder().setWidthDp(1).build()
    +                    : new FixedDividerAttributes.Builder().setWidthDp(1).build();
    +        } else {
    +            dividerAttributes = DividerAttributes.NO_DIVIDER;
    +        }
    +
             final SplitAttributes defaultSplitAttributes = new SplitAttributes.Builder()
                     .setSplitType(SplitAttributes.SplitType.ratio(SPLIT_RATIO))
    +                .setDividerAttributes(dividerAttributes)
                     .build();
     
             if (mViewBinding.splitMainCheckBox.isChecked()) {
    @@ -349,6 +473,8 @@
                 mRuleController.addRule(rule);
             }
     
    +        mViewBinding.draggableDividerCheckBox.setEnabled(mViewBinding.dividerCheckBox.isChecked());
    +
             if (mViewBinding.usePlaceholderCheckBox.isChecked()) {
                 // Split B with placeholder.
                 final Set activityFilters = new HashSet<>();
    @@ -436,11 +562,21 @@
         }
     
         /** Updates the status label that says when an activity is embedded. */
    -    void updateEmbeddedStatus() {
    -        if (ActivityEmbeddingController.getInstance(this).isActivityEmbedded(this)) {
    -            mViewBinding.activityEmbeddedStatusTextView.setVisibility(View.VISIBLE);
    -        } else {
    -            mViewBinding.activityEmbeddedStatusTextView.setVisibility(View.GONE);
    +    private void updateEmbeddedStatus(boolean isEmbedded) {
    +        mViewBinding.activityEmbeddedStatusTextView.setVisibility(isEmbedded
    +                ? View.VISIBLE
    +                : View.GONE);
    +    }
    +
    +    private void updateEmbeddedWindowInfo(
    +            @NonNull EmbeddedActivityWindowInfo info) {
    +        Log.d(TAG, "EmbeddedActivityWindowInfo changed for r=" + this + "\ninfo=" + info);
    +        if (!info.isEmbedded()) {
    +            mViewBinding.activityEmbeddedBoundsTextView.setVisibility(View.GONE);
    +            return;
             }
    +        mViewBinding.activityEmbeddedBoundsTextView.setVisibility(View.VISIBLE);
    +        mViewBinding.activityEmbeddedBoundsTextView.setText(
    +                "Embedded bounds=" + info.getBoundsInParentHost());
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
    index dcbbb3b..7b61240c 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
    
    @@ -17,6 +17,7 @@
     package androidx.window.demo.embedding
     
     import android.graphics.Color
    +import android.graphics.drawable.ColorDrawable
     import android.os.Bundle
     import android.view.View
     import androidx.window.demo.R
    @@ -24,7 +25,8 @@
     open class SplitActivityC : SplitActivityBase() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        findViewById(R.id.root_split_activity_layout)
    -            .setBackgroundColor(Color.parseColor("#e8f5e9"))
    +        val color = Color.parseColor("#e8f5e9")
    +        findViewById(R.id.root_split_activity_layout).setBackgroundColor(color)
    +        window.setBackgroundDrawable(ColorDrawable(color))
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
    index 705dfee..000ff2b 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
    
    @@ -17,6 +17,7 @@
     package androidx.window.demo.embedding
     
     import android.graphics.Color
    +import android.graphics.drawable.ColorDrawable
     import android.os.Bundle
     import android.view.View
     import androidx.window.demo.R
    @@ -24,7 +25,8 @@
     open class SplitActivityD : SplitActivityBase() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        findViewById(R.id.root_split_activity_layout)
    -            .setBackgroundColor(Color.parseColor("#eeeeee"))
    +        val color = Color.parseColor("#eeeeee")
    +        findViewById(R.id.root_split_activity_layout).setBackgroundColor(color)
    +        window.setBackgroundDrawable(ColorDrawable(color))
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
    index f4f6bff..11e8e33 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
    
    @@ -19,27 +19,34 @@
     import android.content.Intent
     import android.graphics.Color
     import android.os.Bundle
    -import android.view.View
     import android.widget.TextView
     import androidx.appcompat.app.AppCompatActivity
    -import androidx.window.demo.R
    +import androidx.window.demo.databinding.ActivitySplitActivityListDetailLayoutBinding
     
     open class SplitActivityDetail : AppCompatActivity() {
    +
    +    private lateinit var viewBinding: ActivitySplitActivityListDetailLayoutBinding
    +    private lateinit var itemDetailTextView: TextView
    +
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        setContentView(R.layout.activity_split_activity_list_detail_layout)
    -        findViewById(R.id.root_split_activity_layout)
    -            .setBackgroundColor(Color.parseColor("#fff3e0"))
     
    -        findViewById(R.id.item_detail_text)
    -            .setText(intent.getStringExtra(EXTRA_SELECTED_ITEM))
    +        viewBinding = ActivitySplitActivityListDetailLayoutBinding.inflate(layoutInflater)
    +        viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#fff3e0"))
    +        setContentView(viewBinding.root)
    +        itemDetailTextView = viewBinding.itemDetailText
    +
    +        itemDetailTextView.text = intent.getStringExtra(EXTRA_SELECTED_ITEM)
    +
    +        window.decorView.setOnFocusChangeListener { _, focus ->
    +            itemDetailTextView.text = "${itemDetailTextView.text} focus=$focus"
    +        }
         }
     
         override fun onNewIntent(intent: Intent) {
             super.onNewIntent(intent)
     
    -        findViewById(R.id.item_detail_text)
    -            .setText(intent.getStringExtra(EXTRA_SELECTED_ITEM))
    +        itemDetailTextView.text = intent.getStringExtra(EXTRA_SELECTED_ITEM)
         }
     
         companion object {
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
    index 2766a7c..a530d22 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
    
    @@ -17,6 +17,7 @@
     package androidx.window.demo.embedding
     
     import android.graphics.Color
    +import android.graphics.drawable.ColorDrawable
     import android.os.Bundle
     import android.view.View
     import androidx.window.demo.R
    @@ -24,7 +25,8 @@
     open class SplitActivityE : SplitActivityBase() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        findViewById(R.id.root_split_activity_layout)
    -            .setBackgroundColor(Color.parseColor("#ede7f6"))
    +        val color = Color.parseColor("#ede7f6")
    +        findViewById(R.id.root_split_activity_layout).setBackgroundColor(color)
    +        window.setBackgroundDrawable(ColorDrawable(color))
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
    index 5fbf9ce..d8161d0 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
    
    @@ -17,6 +17,7 @@
     package androidx.window.demo.embedding
     
     import android.graphics.Color
    +import android.graphics.drawable.ColorDrawable
     import android.os.Bundle
     import android.view.View
     import androidx.window.demo.R
    @@ -24,7 +25,8 @@
     open class SplitActivityF : SplitActivityBase() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
    -        findViewById(R.id.root_split_activity_layout)
    -            .setBackgroundColor(Color.parseColor("#ffebee"))
    +        val color = Color.parseColor("#ffebee")
    +        findViewById(R.id.root_split_activity_layout).setBackgroundColor(color)
    +        window.setBackgroundDrawable(ColorDrawable(color))
         }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
    index f3f3980..f69e35e 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleMainActivity.kt
    
    @@ -77,6 +77,7 @@
                 splitRuleFoldingAwareAttrsRadioButton.isEnabled = false
                 viewBinding.splitRuleUseCustomizedSplitAttributes.isEnabled = false
             }
    +
             viewBinding.startPrimaryActivityButton.setOnClickListener(this)
             viewBinding.useStickyPlaceholderCheckBox.setOnCheckedChangeListener(this)
             viewBinding.usePlaceholderCheckBox.setOnCheckedChangeListener(this)
    @@ -222,6 +223,12 @@
                 if (apiLevel < 3) {
                     append("Finishing secondary activities is not supported on this device!\n")
                 }
    +            if (viewBinding.finishSecondaryActivitiesButton.isEnabled &&
    +                getSplitRule() != null
    +            ) {
    +                append(resources.getString(R.string.show_placeholder_warning))
    +                append("\n")
    +            }
             }
             withContext(Dispatchers.Main) {
                 viewBinding.warningMessageTextView.text = warningMessages
    @@ -406,8 +413,11 @@
                 R.id.placeholder_layout_direction_spinner, R.id.split_rule_layout_direction_spinner ->
                     demoActivityEmbeddingController.customizedLayoutDirection =
                         CUSTOMIZED_LAYOUT_DIRECTIONS_VALUE[position]
    +            R.id.animation_background_dropdown ->
    +                demoActivityEmbeddingController.animationBackground =
    +                    DemoActivityEmbeddingController.ANIMATION_BACKGROUND_VALUES[position]
             }
    -        splitController.invalidateTopVisibleSplitAttributes()
    +        activityEmbeddingController.invalidateVisibleActivityStacks()
         }
     
         override fun onNothingSelected(view: AdapterView<*>?) {
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
    index 71160b5..e80f5a0 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesTogglePrimaryActivity.kt
    
    @@ -21,14 +21,19 @@
     import android.graphics.Color
     import android.os.Bundle
     import android.view.View
    +import android.widget.ArrayAdapter
     import androidx.lifecycle.Lifecycle
     import androidx.lifecycle.lifecycleScope
     import androidx.lifecycle.repeatOnLifecycle
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.demo.R
     import androidx.window.embedding.ActivityStack
     import androidx.window.embedding.SplitInfo
     import kotlinx.coroutines.flow.onEach
     import kotlinx.coroutines.launch
     
    +@OptIn(ExperimentalWindowApi::class)
     class SplitAttributesTogglePrimaryActivity : SplitAttributesToggleMainActivity(),
         View.OnClickListener {
     
    @@ -40,6 +45,8 @@
     
             viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#e8f5e9"))
     
    +        val isRuntimeApiSupported = WindowSdkExtensions.getInstance().extensionVersion >= 3
    +
             secondaryActivityIntent = Intent(
                 this,
                 SplitAttributesToggleSecondaryActivity::class.java
    @@ -54,6 +61,29 @@
     
             // Enable to finish secondary ActivityStacks for primary Activity.
             viewBinding.finishSecondaryActivitiesDivider.visibility = View.VISIBLE
    +        val finishSecondaryActivitiesButton =
    +            viewBinding.finishSecondaryActivitiesButton.apply {
    +                visibility = View.VISIBLE
    +                if (!isRuntimeApiSupported) {
    +                    isEnabled = false
    +                } else {
    +                    setOnClickListener(this@SplitAttributesTogglePrimaryActivity)
    +                }
    +            }
    +
    +        // Animation background
    +        if (WindowSdkExtensions.getInstance().extensionVersion >= 5) {
    +            val animationBackgroundDropdown = viewBinding.animationBackgroundDropdown
    +            animationBackgroundDropdown.visibility = View.VISIBLE
    +            viewBinding.animationBackgroundDivider.visibility = View.VISIBLE
    +            viewBinding.animationBackgroundTextView.visibility = View.VISIBLE
    +            animationBackgroundDropdown.adapter = ArrayAdapter(
    +                this,
    +                android.R.layout.simple_spinner_dropdown_item,
    +                DemoActivityEmbeddingController.ANIMATION_BACKGROUND_TEXTS
    +            )
    +            animationBackgroundDropdown.onItemSelectedListener = this
    +        }
     
             lifecycleScope.launch {
                 // The block passed to repeatOnLifecycle is executed when the lifecycle
    @@ -64,6 +94,7 @@
                         .splitInfoList(this@SplitAttributesTogglePrimaryActivity)
                         .onEach { updateUiFromRules() }
                         .collect { splitInfoList ->
    +                        finishSecondaryActivitiesButton.isEnabled = splitInfoList.isNotEmpty()
                             activityStacks = splitInfoList.mapTo(mutableSetOf()) { splitInfo ->
                                 splitInfo.getTheOtherActivityStack(
                                     this@SplitAttributesTogglePrimaryActivity
    @@ -80,4 +111,14 @@
             } else {
                 primaryActivityStack
             }
    +
    +    override fun onClick(button: View) {
    +        super.onClick(button)
    +        when (button.id) {
    +            R.id.finish_secondary_activities_button -> {
    +                applyRules()
    +                activityEmbeddingController.finishActivityStacks(activityStacks)
    +            }
    +        }
    +    }
     }
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
    index 01104cb..000252e 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitAttributesToggleSecondaryActivity.kt
    
    @@ -165,7 +165,7 @@
                         val enableFullscreenMode = DemoActivityEmbeddingController.getInstance()
                             .shouldExpandSecondaryContainer
                         enableFullscreenMode.set(!enableFullscreenMode.get())
    -                    splitController.invalidateTopVisibleSplitAttributes()
    +                    activityEmbeddingController.invalidateVisibleActivityStacks()
                     } else {
                         // Update the top splitInfo if single default split Attributes is used.
                         splitController.updateSplitAttributes(
    @@ -192,7 +192,7 @@
                     demoActivityEmbeddingController.customizedLayoutDirection =
                         CUSTOMIZED_LAYOUT_DIRECTIONS_VALUE[position]
             }
    -        splitController.invalidateTopVisibleSplitAttributes()
    +        activityEmbeddingController.invalidateVisibleActivityStacks()
         }
     
         override fun onNothingSelected(view: AdapterView<*>?) {
    
    diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
    index fcb0ddb..3b29164 100644
    --- a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
    +++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
    
    @@ -21,6 +21,7 @@
     import android.os.Bundle
     import android.view.View
     import android.widget.AdapterView
    +import android.widget.ArrayAdapter
     import android.widget.CompoundButton
     import android.widget.RadioGroup
     import android.widget.Toast
    @@ -37,7 +38,8 @@
     import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
     import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
     import androidx.window.embedding.SplitController
    -import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
    +import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_ERROR_PROPERTY_NOT_DECLARED
    +import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_UNAVAILABLE
     import androidx.window.embedding.SplitInfo
     import androidx.window.embedding.SplitPairFilter
     import androidx.window.embedding.SplitPairRule
    @@ -61,6 +63,8 @@
         private lateinit var activityA: ComponentName
         private lateinit var activityB: ComponentName
     
    +    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
    +
         /** The last selected split rule id. */
         private var lastCheckedRuleId = 0
     
    @@ -70,7 +74,14 @@
             super.onCreate(savedInstanceState)
             viewBinding = ActivitySplitDeviceStateLayoutBinding.inflate(layoutInflater)
             splitController = SplitController.getInstance(this)
    -        if (splitController.splitSupportStatus != SPLIT_AVAILABLE) {
    +        if (splitController.splitSupportStatus == SPLIT_UNAVAILABLE) {
    +            Toast.makeText(
    +                this, R.string.toast_split_not_available,
    +                Toast.LENGTH_SHORT
    +            ).show()
    +            finish()
    +            return
    +        } else if (splitController.splitSupportStatus == SPLIT_ERROR_PROPERTY_NOT_DECLARED) {
                 Toast.makeText(
                     this, R.string.toast_split_not_support,
                     Toast.LENGTH_SHORT
    @@ -118,6 +129,21 @@
                     .getString(R.string.split_attributes_calculator_not_supported)
             }
     
    +        // Animation background
    +        if (WindowSdkExtensions.getInstance().extensionVersion >= 5 && componentName == activityA) {
    +            // Show on only the primary activity.
    +            val animationBackgroundDropdown = viewBinding.animationBackgroundDropdown
    +            animationBackgroundDropdown.visibility = View.VISIBLE
    +            viewBinding.animationBackgroundDivider.visibility = View.VISIBLE
    +            viewBinding.animationBackgroundTextView.visibility = View.VISIBLE
    +            animationBackgroundDropdown.adapter = ArrayAdapter(
    +                this,
    +                android.R.layout.simple_spinner_dropdown_item,
    +                DemoActivityEmbeddingController.ANIMATION_BACKGROUND_TEXTS
    +            )
    +            animationBackgroundDropdown.onItemSelectedListener = this
    +        }
    +
             lifecycleScope.launch {
                 // The block passed to repeatOnLifecycle is executed when the lifecycle
                 // is at least STARTED and is cancelled when the lifecycle is STOPPED.
    @@ -173,6 +199,8 @@
         }
     
         override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
    +        demoActivityEmbeddingController.animationBackground =
    +            DemoActivityEmbeddingController.ANIMATION_BACKGROUND_VALUES[position]
             updateSplitPairRuleWithRadioButtonId(lastCheckedRuleId)
         }
     
    @@ -250,6 +278,7 @@
             val defaultSplitAttributes = SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_EQUAL)
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    +            .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
                 .build()
             // Use the tag to control the rule how to change split attributes with the current state
             var tag = when (id) {
    @@ -295,6 +324,7 @@
         private suspend fun updateSplitAttributesText(newSplitInfos: List) {
             var splitAttributes: SplitAttributes = SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_EXPAND)
    +            .setAnimationBackground(demoActivityEmbeddingController.animationBackground)
                 .build()
             var suggestToFinishItself = false
     
    
    diff --git a/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml
    new file mode 100644
    index 0000000..8650bcb
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/res/layout/activity_overlay_activity_layout.xml
    
    @@ -0,0 +1,226 @@
    +
    +
    +
    +    xmlns:tools="http://schemas.android.com/tools"
    +    android:id="@+id/root_overlay_activity_layout"
    +    android:layout_width="match_parent"
    +    android:layout_height="match_parent">
    +
    +    
    +        android:layout_width="match_parent"
    +        android:layout_height="wrap_content"
    +        android:orientation="vertical">
    +
    +        
    +            android:id="@+id/textView_overlay_bounds"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="@string/overlay_bounds_text" />
    +
    +        
    +            android:layout_width="match_parent"
    +            android:layout_height="1dp"
    +            android:layout_marginBottom="10dp"
    +            android:layout_marginTop="10dp"
    +            android:background="#AAAAAA" />
    +
    +        
    +            android:id="@+id/textView_choose_overlay_layout"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Choose overlay layout:" />
    +
    +        
    +            android:id="@+id/radioGroup_choose_overlay_layout"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content">
    +
    +            
    +                android:id="@+id/radioButton_simple_overlay"
    +                android:layout_width="match_parent"
    +                android:layout_height="wrap_content"
    +                android:text="Launch the overlay container to the right" />
    +
    +            
    +                android:id="@+id/radioButton_change_with_orientation"
    +                android:layout_width="match_parent"
    +                android:layout_height="wrap_content"
    +                android:text="Change overlay layout with orientation" />
    +
    +            
    +                android:id="@+id/radioButton_customization"
    +                android:layout_width="match_parent"
    +                android:layout_height="wrap_content"
    +                android:text="Use customized overlay layout" />
    +        
    +
    +        
    +            android:id="@+id/textView_layout"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Layout"
    +            android:visibility="visible" />
    +
    +        
    +
    +        
    +            android:id="@+id/textView_position"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Position"
    +            android:visibility="visible" />
    +        
    +            android:id="@+id/spinner_alignment"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:spinnerMode="dropdown"
    +            tools:visibility="visible" />
    +
    +        
    +
    +        
    +            android:id="@+id/textView_width"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Width"
    +            android:visibility="visible" />
    +        
    +            android:id="@+id/spinner_width"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:spinnerMode="dropdown"
    +            tools:visibility="visible" />
    +        
    +            android:id="@+id/textView_width_in_pixel"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Width in pixel"
    +            android:visibility="gone" />
    +        
    +            android:id="@+id/editTextNumberDecimal_width_in_pixel"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:ems="4"
    +            android:inputType="numberDecimal"
    +            android:visibility="gone" />
    +        
    +            android:id="@+id/textView_width_in_ratio"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="@string/width_in_ratio" />
    +        
    +            android:id="@+id/seekBar_width_in_ratio"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:min="1"
    +            android:max="99"
    +            android:progress="50" />
    +
    +        
    +
    +        
    +            android:id="@+id/textView_height"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Height"
    +            android:visibility="visible" />
    +        
    +            android:id="@+id/spinner_height"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:spinnerMode="dropdown"
    +            tools:visibility="visible" />
    +        
    +            android:id="@+id/textView_height_in_pixel"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Height in pixel"
    +            android:visibility="gone" />
    +        
    +            android:id="@+id/editTextNumberDecimal_height_in_pixel"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:ems="4"
    +            android:inputType="numberDecimal"
    +            android:visibility="gone" />
    +        
    +            android:id="@+id/textView_height_in_ratio"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="@string/height_in_ratio"
    +            android:visibility="gone" />
    +        
    +            android:id="@+id/seekBar_height_in_ratio"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:min="1"
    +            android:max="99"
    +            android:progress="80"
    +            android:visibility="gone" />
    +
    +        
    +
    +        
    +            android:id="@+id/button_update_overlay_layout"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Update Overlay layout" />
    +
    +        
    +            android:layout_width="match_parent"
    +            android:layout_height="1dp"
    +            android:layout_marginBottom="10dp"
    +            android:layout_marginTop="10dp"
    +            android:background="#AAAAAA" />
    +
    +        
    +            android:id="@+id/button_launch_overlay_container"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Launch the overlay container" />
    +
    +        
    +            android:id="@+id/checkbox_reorder_to_front"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Reorder Activity A to the front" />
    +
    +        
    +            android:id="@+id/button_launch_overlay_activity_a"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Launch Overlay Associated Activity A" />
    +
    +        
    +            android:id="@+id/checkbox_launch_to_overlay"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Launch Activity B to the overlay container" />
    +
    +        
    +            android:id="@+id/button_launch_overlay_activity_b"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Launch Overlay Associated Activity B" />
    +
    +        
    +            android:id="@+id/button_finish_this_activity"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Finish this activity" />
    +    
    +
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
    index 43bea60..9d0c9c3 100644
    --- a/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
    +++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
    
    @@ -37,6 +37,17 @@
             android:layout_marginBottom="32dp"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
    +        app:layout_constraintBottom_toTopOf="@id/rear_display_session_button" />
    +
    +    
    +        android:id="@+id/rear_display_session_button"
    +        android:text="Get active session if available"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:textAlignment="center"
    +        android:layout_marginBottom="32dp"
    +        app:layout_constraintStart_toStartOf="parent"
    +        app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintBottom_toBottomOf="parent" />
     
     
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml b/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml
    new file mode 100644
    index 0000000..ff5129d
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display_presentation.xml
    
    @@ -0,0 +1,42 @@
    +
    +
    +
    +    android:layout_width="match_parent"
    +    android:layout_height="match_parent"
    +    xmlns:app="http://schemas.android.com/apk/res-auto">
    +
    +    
    +        android:id="@+id/rearStatusRecyclerView"
    +        android:layout_width="0dp"
    +        android:layout_height="wrap_content"
    +        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    +        app:layout_constraintEnd_toEndOf="parent"
    +        app:layout_constraintStart_toStartOf="parent"
    +        app:layout_constraintTop_toTopOf="parent"/>
    +
    +    
    +        android:id="@+id/rear_display_presentation_button"
    +        android:text="Enable Rear Display Presentation"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:textAlignment="center"
    +        android:layout_marginBottom="32dp"
    +        app:layout_constraintStart_toStartOf="parent"
    +        app:layout_constraintEnd_toEndOf="parent"
    +        app:layout_constraintBottom_toBottomOf="parent" />
    +
    +
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
    index ab54e1e..bb565ce 100644
    --- a/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
    +++ b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
    
    @@ -30,7 +30,15 @@
                 android:id="@+id/activity_embedded_status_text_view"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
    -            android:text="Activity is embedded" />
    +            android:text="Activity is embedded"
    +            android:visibility="gone" />
    +
    +        
    +            android:id="@+id/activity_embedded_bounds_text_view"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Embedded bounds not available"
    +            android:visibility="gone" />
     
             
                 android:id="@+id/splitMainCheckBox"
    @@ -38,6 +46,19 @@
                 android:layout_height="wrap_content"
                 android:text="Split Main with other activities" />
     
    +        
    +            android:id="@+id/dividerCheckBox"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Add a divider between containers" />
    +
    +        
    +            android:id="@+id/draggableDividerCheckBox"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Make the divider draggable"
    +            android:enabled="false"/>
    +
             
                 android:layout_width="match_parent"
                 android:layout_height="1dp"
    @@ -193,5 +214,60 @@
                 android:layout_height="wrap_content"
                 android:layout_centerHorizontal="true"
                 android:text="Launch Expanded Dialog" />
    +
    +        
    +            android:id="@+id/launch_dialog_activity_button"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:layout_centerHorizontal="true"
    +            android:text="Launch Dialog Activity" />
    +
    +        
    +            android:id="@+id/launch_dialog_button"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:layout_centerHorizontal="true"
    +            android:text="Launch Dialog" />
    +
    +        
    +            android:layout_width="match_parent"
    +            android:layout_height="1dp"
    +            android:layout_marginTop="10dp"
    +            android:layout_marginBottom="10dp"
    +            android:background="#AAAAAA" />
    +
    +        
    +            android:id="@+id/pin_top_activity_stack_button"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:layout_centerHorizontal="true"
    +            android:text="Pin Top ActivityStack" />
    +
    +        
    +            android:id="@+id/stickyPinRule"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="Set Pin Rule Sticky" />
    +
    +        
    +            android:id="@+id/unpin_top_activity_stack_button"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:layout_centerHorizontal="true"
    +            android:text="Unpin Top ActivityStack" />
    +
    +        
    +            android:layout_width="match_parent"
    +            android:layout_height="1dp"
    +            android:layout_marginTop="10dp"
    +            android:layout_marginBottom="10dp"
    +            android:background="#AAAAAA" />
    +
    +        
    +            android:id="@+id/button_launch_overlay_associated_activity"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:layout_centerHorizontal="true"
    +            android:text="Launch Overlay Associated Activity A" />
         
     
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml b/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
    index dad1100..c4e0ef7 100644
    --- a/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
    +++ b/window/window-demos/demo/src/main/res/layout/activity_split_attributes_toggle_primary_activity.xml
    
    @@ -192,6 +192,31 @@
                         android:visibility="gone"/>
             
     
    +        
    +
    +        
    +            android:id="@+id/animation_background_divider"
    +            android:layout_width="match_parent"
    +            android:layout_height="1dp"
    +            android:layout_marginTop="10dp"
    +            android:layout_marginBottom="10dp"
    +            android:background="#AAAAAA"
    +            android:visibility="gone"/>
    +
    +        
    +            android:id="@+id/animation_background_text_view"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="@string/current_animation_background"
    +            android:visibility="gone"/>
    +
    +        
    +            android:id="@+id/animation_background_dropdown"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:spinnerMode="dropdown"
    +            android:visibility="gone"/>
    +
             
                 android:id="@+id/finish_secondary_activities_divider"
                 android:layout_width="match_parent"
    @@ -201,6 +226,14 @@
                 android:visibility="gone"
                 android:background="#AAAAAA" />
     
    +        
    +            android:id="@+id/finish_secondary_activities_button"
    +            android:layout_width="wrap_content"
    +            android:layout_height="48dp"
    +            android:layout_centerHorizontal="true"
    +            android:text="Finish secondary activities"
    +            android:visibility="gone"/>
    +
             
                 android:layout_width="match_parent"
                 android:layout_height="1dp"
    
    diff --git a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
    index c71a541..4bd1b21 100644
    --- a/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
    +++ b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
    
    @@ -143,6 +143,31 @@
                     android:text="Swap the position of primary and secondary container" />
             
     
    +        
    +
    +        
    +            android:id="@+id/animation_background_divider"
    +            android:layout_width="match_parent"
    +            android:layout_height="1dp"
    +            android:layout_marginTop="10dp"
    +            android:layout_marginBottom="10dp"
    +            android:background="#AAAAAA"
    +            android:visibility="gone"/>
    +
    +        
    +            android:id="@+id/animation_background_text_view"
    +            android:layout_width="match_parent"
    +            android:layout_height="wrap_content"
    +            android:text="@string/current_animation_background"
    +            android:visibility="gone"/>
    +
    +        
    +            android:id="@+id/animation_background_dropdown"
    +            android:layout_width="wrap_content"
    +            android:layout_height="wrap_content"
    +            android:spinnerMode="dropdown"
    +            android:visibility="gone"/>
    +
             
                 android:id="@+id/launch_activity_to_side"
                 android:layout_width="wrap_content"
    
    diff --git a/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml b/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
    new file mode 100644
    index 0000000..9c10df2
    --- /dev/null
    +++ b/window/window-demos/demo/src/main/res/layout/concurrent_presentation.xml
    
    @@ -0,0 +1,34 @@
    +
    +
    +
    +
    +    xmlns:android="http://schemas.android.com/apk/res/android"
    +    xmlns:app="http://schemas.android.com/apk/res-auto"
    +    android:layout_width="match_parent"
    +    android:layout_height="match_parent">
    +
    +    
    +        android:id="@+id/textView"
    +        android:layout_width="wrap_content"
    +        android:layout_height="wrap_content"
    +        android:text="Dual Display Presentation"
    +        app:layout_constraintBottom_toBottomOf="parent"
    +        app:layout_constraintEnd_toEndOf="parent"
    +        app:layout_constraintStart_toStartOf="parent"
    +        app:layout_constraintTop_toTopOf="parent" />
    +
    +
    \ No newline at end of file
    
    diff --git a/window/window-demos/demo/src/main/res/values/strings.xml b/window/window-demos/demo/src/main/res/values/strings.xml
    index 892cecc..6646e27 100644
    --- a/window/window-demos/demo/src/main/res/values/strings.xml
    +++ b/window/window-demos/demo/src/main/res/values/strings.xml
    
    @@ -50,7 +50,7 @@
         Rear Display Mode
         Demo of observing to WindowAreaStatus and enabling/disabling RearDisplay mode
         Current SplitAttributes:>
    -    Current Animation Background Color:>
    +    Current Animation Background:>
         Test IME
         Clear Logs
         Close Test IME
    @@ -61,7 +61,8 @@
         System IME Settings
         Switch default IME
         Install window-demos:demo-second-app to launch activities from a different UID.
    -    Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED and ensure device supports splits.
    +    This device does not support Activity Embedding.
    +    Please enable PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED.
         Latest Configuration
         Application DisplayListener#onDisplayChanged
         Activity DisplayListener#onDisplayChanged
    @@ -86,4 +87,10 @@
         Choose the split type
         Choose the layout direction
         Placeholder Activity may show (again) because "Use a placeholder for A is checked". Clear the check box to expand Activity A to fill the task by either clicking "FINISH SECONDARY ACTIVITIES".
    +    Dual Display
    +    Demo of showing content on the rear and internal displays concurrently
    +    The device does not support overlay features!
    +    Overlay bounds:
    +    Width in ratio: 
    +    Height in ratio: 
     
    
    diff --git a/window/window-demos/demo/src/main/res/xml/main_split_config.xml b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
    index 8269c38..3a898ce 100644
    --- a/window/window-demos/demo/src/main/res/xml/main_split_config.xml
    +++ b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
    
    @@ -41,4 +41,14 @@
             
                 window:activityName="androidx.window.demo.embedding.SplitImeActivityMain"/>
         
    +
    +    
    +
    +    
    +        window:finishPrimaryWithSecondary="never"
    +        window:finishSecondaryWithPrimary="adjacent">
    +        
    +            window:primaryActivityName="androidx.window.demo.embedding.OverlayActivityA"
    +            window:secondaryActivityName="androidx.window.demo.embedding.SplitActivityDetail"/>
    +    
     
    \ No newline at end of file
    
    diff --git a/window/window-java/api/current.txt b/window/window-java/api/current.txt
    index 2e19128..c3b2d91 100644
    --- a/window/window-java/api/current.txt
    +++ b/window/window-java/api/current.txt
    
    @@ -11,6 +11,18 @@
     
     package androidx.window.java.embedding {
     
    +  public final class ActivityEmbeddingControllerCallbackAdapter {
    +    ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer listener);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer listener);
    +  }
    +
    +  public final class OverlayControllerCallbackAdapter {
    +    ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer consumer);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer consumer);
    +  }
    +
       @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
         ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller);
         method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer> consumer);
    
    diff --git a/window/window-java/api/restricted_current.txt b/window/window-java/api/restricted_current.txt
    index 2e19128..c3b2d91 100644
    --- a/window/window-java/api/restricted_current.txt
    +++ b/window/window-java/api/restricted_current.txt
    
    @@ -11,6 +11,18 @@
     
     package androidx.window.java.embedding {
     
    +  public final class ActivityEmbeddingControllerCallbackAdapter {
    +    ctor public ActivityEmbeddingControllerCallbackAdapter(androidx.window.embedding.ActivityEmbeddingController controller);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void addEmbeddedActivityWindowInfoListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer listener);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void removeEmbeddedActivityWindowInfoListener(androidx.core.util.Consumer listener);
    +  }
    +
    +  public final class OverlayControllerCallbackAdapter {
    +    ctor public OverlayControllerCallbackAdapter(androidx.window.embedding.OverlayController controller);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void addOverlayInfoListener(String overlayTag, java.util.concurrent.Executor executor, androidx.core.util.Consumer consumer);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void removeOverlayInfoListener(androidx.core.util.Consumer consumer);
    +  }
    +
       @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public final class SplitControllerCallbackAdapter {
         ctor public SplitControllerCallbackAdapter(androidx.window.embedding.SplitController controller);
         method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer> consumer);
    
    diff --git a/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt
    new file mode 100644
    index 0000000..2338993
    --- /dev/null
    +++ b/window/window-java/src/main/java/androidx/window/java/embedding/ActivityEmbeddingControllerCallbackAdapter.kt
    
    @@ -0,0 +1,91 @@
    +/*
    + * 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 androidx.window.java.embedding
    +
    +import android.app.Activity
    +import androidx.core.util.Consumer
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.embedding.ActivityEmbeddingController
    +import androidx.window.embedding.EmbeddedActivityWindowInfo
    +import androidx.window.java.core.CallbackToFlowAdapter
    +import java.util.concurrent.Executor
    +
    +/**
    + * An adapted interface for [ActivityEmbeddingController] that provides callback shaped APIs to
    + * report the latest [EmbeddedActivityWindowInfo].
    + *
    + * It should only be used if [ActivityEmbeddingController.embeddedActivityWindowInfo] is not
    + * available. For example, an app is written in Java and cannot use Flow APIs.
    + *
    + * @constructor creates a callback adapter of
    + * [ActivityEmbeddingController.embeddedActivityWindowInfo] flow API.
    + * @param controller an [ActivityEmbeddingController] that can be obtained by
    + * [ActivityEmbeddingController.getInstance].
    + */
    +class ActivityEmbeddingControllerCallbackAdapter(
    +    private val controller: ActivityEmbeddingController
    +) {
    +    private val callbackToFlowAdapter = CallbackToFlowAdapter()
    +
    +    /**
    +     * Registers a listener for updates of [EmbeddedActivityWindowInfo] of [activity].
    +     *
    +     * The [listener] will immediately be invoked with the latest value upon registration if the
    +     * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.
    +     *
    +     * When the [activity] is embedded, the [listener] will be invoked when
    +     * [EmbeddedActivityWindowInfo] is changed.
    +     * When the [activity] is not embedded, the [listener] will not be triggered unless the
    +     * [activity] is becoming non-embedded from embedded.
    +     *
    +     * Note that this API is only supported on the device with
    +     * [WindowSdkExtensions.extensionVersion] equal to or larger than 6.
    +     * If [WindowSdkExtensions.extensionVersion] is less than 6, this [listener] will not be
    +     * invoked.
    +     *
    +     * @param activity the [Activity] that is interested in getting the embedded window info.
    +     * @param executor the [Executor] to dispatch the [EmbeddedActivityWindowInfo] change.
    +     * @param listener the [Consumer] that will be invoked on the [executor] when there is
    +     * an update to [EmbeddedActivityWindowInfo].
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun addEmbeddedActivityWindowInfoListener(
    +        activity: Activity,
    +        executor: Executor,
    +        listener: Consumer
    +    ) {
    +        callbackToFlowAdapter.connect(
    +            executor,
    +            listener,
    +            controller.embeddedActivityWindowInfo(activity)
    +        )
    +    }
    +
    +    /**
    +     * Unregisters a listener that was previously registered via
    +     * [addEmbeddedActivityWindowInfoListener].
    +     *
    +     * It's no-op if the [listener] has not been registered.
    +     *
    +     * @param listener the previously registered [Consumer] to unregister.
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun removeEmbeddedActivityWindowInfoListener(listener: Consumer) {
    +        callbackToFlowAdapter.disconnect(listener)
    +    }
    +}
    
    diff --git a/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt b/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt
    new file mode 100644
    index 0000000..0bea3014
    --- /dev/null
    +++ b/window/window-java/src/main/java/androidx/window/java/embedding/OverlayControllerCallbackAdapter.kt
    
    @@ -0,0 +1,78 @@
    +/*
    + * 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 androidx.window.java.embedding
    +
    +import androidx.core.util.Consumer
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.embedding.ActivityStack
    +import androidx.window.embedding.OverlayController
    +import androidx.window.embedding.OverlayCreateParams
    +import androidx.window.embedding.OverlayInfo
    +import androidx.window.java.core.CallbackToFlowAdapter
    +import java.util.concurrent.Executor
    +
    +/**
    + * An adapted interface for [OverlayController] that provides callback shaped APIs to report
    + * the latest [OverlayInfo].
    + *
    + * It should only be used if [OverlayController.overlayInfo] is not available. For example, an app
    + * is written in Java and cannot use Flow APIs.
    + *
    + * @constructor creates a callback adapter of [OverlayController.overlayInfo] flow API.
    + * @param controller an [OverlayController] that can be obtained by [OverlayController.getInstance].
    + */
    +class OverlayControllerCallbackAdapter(private val controller: OverlayController) {
    +
    +    private val callbackToFlowAdapter = CallbackToFlowAdapter()
    +
    +    /**
    +     * Registers a listener for updates of [OverlayInfo] that [overlayTag] is associated with.
    +     *
    +     * If there is no active overlay [ActivityStack], the reported [OverlayInfo.activityStack] and
    +     * [OverlayInfo.currentOverlayAttributes] will be `null`.
    +     *
    +     * Note that launching an overlay [ActivityStack] only supports on the device with
    +     * [WindowSdkExtensions.extensionVersion] equal to or larger than 5.
    +     * If [WindowSdkExtensions.extensionVersion] is less than 5, this flow will always
    +     * report [OverlayInfo] without associated [OverlayInfo.activityStack].
    +     *
    +     * @param overlayTag the overlay [ActivityStack]'s tag which is set through
    +     * [OverlayCreateParams]
    +     * @param executor the [Executor] to dispatch the [OverlayInfo] change
    +     * @param consumer the [Consumer] that will be invoked on the [executor] when there is
    +     * an update to [OverlayInfo].
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    fun addOverlayInfoListener(
    +        overlayTag: String,
    +        executor: Executor,
    +        consumer: Consumer
    +    ) {
    +        callbackToFlowAdapter.connect(executor, consumer, controller.overlayInfo(overlayTag))
    +    }
    +
    +    /**
    +     * Unregisters a listener that was previously registered via [addOverlayInfoListener].
    +     *
    +     * @param consumer the previously registered [Consumer] to unregister.
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    fun removeOverlayInfoListener(consumer: Consumer) {
    +        callbackToFlowAdapter.disconnect(consumer)
    +    }
    +}
    
    diff --git a/window/window-testing/api/current.txt b/window/window-testing/api/current.txt
    index 847a9e8..84d25ac 100644
    --- a/window/window-testing/api/current.txt
    +++ b/window/window-testing/api/current.txt
    
    @@ -1,4 +1,14 @@
     // Signature format: 4.0
    +package androidx.window.testing {
    +
    +  public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {
    +    ctor public WindowSdkExtensionsRule();
    +    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
    +    method public void overrideExtensionVersion(@IntRange(from=0L) int version);
    +  }
    +
    +}
    +
     package androidx.window.testing.embedding {
     
       public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
    
    diff --git a/window/window-testing/api/restricted_current.txt b/window/window-testing/api/restricted_current.txt
    index 847a9e8..84d25ac 100644
    --- a/window/window-testing/api/restricted_current.txt
    +++ b/window/window-testing/api/restricted_current.txt
    
    @@ -1,4 +1,14 @@
     // Signature format: 4.0
    +package androidx.window.testing {
    +
    +  public final class WindowSdkExtensionsRule implements org.junit.rules.TestRule {
    +    ctor public WindowSdkExtensionsRule();
    +    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
    +    method public void overrideExtensionVersion(@IntRange(from=0L) int version);
    +  }
    +
    +}
    +
     package androidx.window.testing.embedding {
     
       public final class ActivityEmbeddingRule implements org.junit.rules.TestRule {
    
    diff --git a/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt b/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt
    new file mode 100644
    index 0000000..79da16d
    --- /dev/null
    +++ b/window/window-testing/src/main/java/androidx/window/testing/FakeWindowSdkExtensions.kt
    
    @@ -0,0 +1,37 @@
    +/*
    + * 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 androidx.window.testing
    +
    +import androidx.annotation.IntRange
    +import androidx.window.WindowSdkExtensions
    +
    +/**
    + * A fake [WindowSdkExtensions] implementation that can override [extensionVersion],
    + * which is intended to be used during unit tests.
    + */
    +internal class FakeWindowSdkExtensions : WindowSdkExtensions() {
    +
    +    override val extensionVersion: Int
    +        get() = _extensionVersion
    +
    +    private var _extensionVersion: Int = 0
    +
    +    internal fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
    +        require(version >= 0) { "The override version must equal to or greater than 0." }
    +        _extensionVersion = version
    +    }
    +}
    
    diff --git a/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt b/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt
    new file mode 100644
    index 0000000..38f834b
    --- /dev/null
    +++ b/window/window-testing/src/main/java/androidx/window/testing/WindowSdkExtensionsRule.kt
    
    @@ -0,0 +1,67 @@
    +/*
    + * 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 androidx.window.testing
    +
    +import androidx.annotation.IntRange
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.WindowSdkExtensionsDecorator
    +import org.junit.rules.TestRule
    +import org.junit.runner.Description
    +import org.junit.runners.model.Statement
    +
    +/**
    + * [TestRule] for overriding [WindowSdkExtensions] properties in unit tests.
    + *
    + * The [TestRule] is designed to only be used in unit tests. Users should use the actual
    + * [WindowSdkExtensions] properties for instrumentation tests. Overriding the device's
    + * extensions version to a higher version may lead to unexpected test failures or even app
    + * crash.
    + */
    +class WindowSdkExtensionsRule : TestRule {
    +
    +    private val fakeWindowSdkExtensions = FakeWindowSdkExtensions()
    +
    +    override fun apply(
    +        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
    +        base: Statement,
    +        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
    +        description: Description
    +    ): Statement {
    +        return object : Statement() {
    +            override fun evaluate() {
    +                WindowSdkExtensions.overrideDecorator(object : WindowSdkExtensionsDecorator {
    +                    override fun decorate(windowSdkExtensions: WindowSdkExtensions):
    +                        WindowSdkExtensions = fakeWindowSdkExtensions
    +                })
    +                try {
    +                    base.evaluate()
    +                } finally {
    +                    WindowSdkExtensions.reset()
    +                }
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Overrides the [WindowSdkExtensions.extensionVersion] for testing.
    +     *
    +     * @param version The extension version to override.
    +     */
    +    fun overrideExtensionVersion(@IntRange(from = 0) version: Int) {
    +        fakeWindowSdkExtensions.overrideExtensionVersion(version)
    +    }
    +}
    
    diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
    index 26a7086..6dc2f84 100644
    --- a/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
    +++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/ActivityStackTesting.kt
    
    @@ -18,9 +18,6 @@
     package androidx.window.testing.embedding
     
     import android.app.Activity
    -import android.os.Binder
    -import androidx.annotation.RestrictTo
    -import androidx.annotation.VisibleForTesting
     import androidx.window.embedding.ActivityStack
     
     /**
    @@ -41,8 +38,3 @@
         activitiesInProcess: List = emptyList(),
         isEmpty: Boolean = false,
     ): ActivityStack = ActivityStack(activitiesInProcess, isEmpty)
    -
    -@RestrictTo(RestrictTo.Scope.LIBRARY)
    -@VisibleForTesting
    -@JvmField
    -val TEST_ACTIVITY_STACK_TOKEN = Binder()
    
    diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
    index b010299..2127101 100644
    --- a/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
    +++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/SplitInfoTesting.kt
    
    @@ -17,7 +17,6 @@
     
     package androidx.window.testing.embedding
     
    -import android.os.Binder
     import androidx.window.embedding.ActivityStack
     import androidx.window.embedding.SplitAttributes
     import androidx.window.embedding.SplitInfo
    @@ -47,7 +46,4 @@
         primaryActivityStack,
         secondActivityStack,
         splitAttributes,
    -    TEST_SPLIT_INFO_TOKEN
     )
    -
    -private val TEST_SPLIT_INFO_TOKEN = Binder()
    
    diff --git a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
    index c1f3fb1..87e12b5 100644
    --- a/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
    +++ b/window/window-testing/src/main/java/androidx/window/testing/embedding/StubEmbeddingBackend.kt
    
    @@ -17,17 +17,24 @@
     package androidx.window.testing.embedding
     
     import android.app.Activity
    -import android.app.ActivityOptions
    -import android.os.IBinder
    +import android.os.Bundle
     import androidx.core.util.Consumer
    +import androidx.window.core.ExperimentalWindowApi
     import androidx.window.embedding.ActivityStack
    +import androidx.window.embedding.EmbeddedActivityWindowInfo
     import androidx.window.embedding.EmbeddingBackend
    +import androidx.window.embedding.EmbeddingConfiguration
     import androidx.window.embedding.EmbeddingRule
    +import androidx.window.embedding.OverlayAttributes
    +import androidx.window.embedding.OverlayAttributesCalculatorParams
    +import androidx.window.embedding.OverlayCreateParams
    +import androidx.window.embedding.OverlayInfo
     import androidx.window.embedding.SplitAttributes
     import androidx.window.embedding.SplitAttributesCalculatorParams
     import androidx.window.embedding.SplitController
     import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_UNAVAILABLE
     import androidx.window.embedding.SplitInfo
    +import androidx.window.embedding.SplitPinRule
     import java.util.concurrent.Executor
     import kotlinx.coroutines.CoroutineScope
     import kotlinx.coroutines.Job
    @@ -149,6 +156,15 @@
         override fun isActivityEmbedded(activity: Activity): Boolean =
             embeddedActivities.contains(activity)
     
    +    @OptIn(ExperimentalWindowApi::class)
    +    override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun unpinTopActivityStack(taskId: Int) {
    +        TODO("Not yet implemented")
    +    }
    +
         override fun setSplitAttributesCalculator(
             calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
         ) {
    @@ -164,13 +180,29 @@
         }
     
         override fun setLaunchingActivityStack(
    -        options: ActivityOptions,
    -        token: IBinder
    -    ): ActivityOptions {
    +        options: Bundle,
    +        activityStack: ActivityStack,
    +    ): Bundle {
             TODO("Not yet implemented")
         }
     
    -    override fun invalidateTopVisibleSplitAttributes() {
    +    override fun setOverlayCreateParams(
    +        options: Bundle,
    +        overlayCreateParams: OverlayCreateParams
    +    ): Bundle {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun finishActivityStacks(activityStacks: Set) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    @OptIn(ExperimentalWindowApi::class)
    +    override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun invalidateVisibleActivityStacks() {
             TODO("Not yet implemented")
         }
     
    @@ -178,6 +210,45 @@
             TODO("Not yet implemented")
         }
     
    +    override fun setOverlayAttributesCalculator(
    +        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
    +    ) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun clearOverlayAttributesCalculator() {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun addOverlayInfoCallback(
    +        overlayTag: String,
    +        executor: Executor,
    +        overlayInfoCallback: Consumer
    +    ) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun removeOverlayInfoCallback(overlayInfoCallback: Consumer) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun addEmbeddedActivityWindowInfoCallbackForActivity(
    +        activity: Activity,
    +        callback: Consumer
    +    ) {
    +        TODO("Not yet implemented")
    +    }
    +
    +    override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
    +        callback: Consumer
    +    ) {
    +        TODO("Not yet implemented")
    +    }
    +
         private fun validateRules(rules: Set) {
             val tags = HashSet()
             rules.forEach { rule ->
    
    diff --git a/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt b/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
    index f59e5ae..af1ee51 100644
    --- a/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
    +++ b/window/window-testing/src/main/java/androidx/window/testing/layout/StubWindowMetricsCalculator.kt
    
    @@ -39,13 +39,13 @@
         override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
             val displayMetrics = activity.resources.displayMetrics
             val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
    -        return WindowMetrics(bounds)
    +        return WindowMetrics(bounds, density = displayMetrics.density)
         }
     
         override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
             val displayMetrics = activity.resources.displayMetrics
             val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
    -        return WindowMetrics(bounds)
    +        return WindowMetrics(bounds, density = displayMetrics.density)
         }
     
         // WindowManager#getDefaultDisplay is deprecated but we have this for compatibility with
    @@ -53,9 +53,10 @@
         @Suppress("DEPRECATION")
         override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
             val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    +        val density = context.resources.displayMetrics.density
     
             return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    -            Api30Impl.getWindowMetrics(wm)
    +            Api30Impl.getWindowMetrics(wm, context)
             } else {
                 val displaySize = Point()
                 // We use getRealSize instead of getSize here because:
    @@ -67,7 +68,7 @@
                 //      getRealSize.
                 wm.defaultDisplay.getRealSize(displaySize)
                 val bounds = Rect(0, 0, displaySize.x, displaySize.y)
    -            WindowMetrics(bounds)
    +            WindowMetrics(bounds, density = density)
             }
         }
     
    @@ -77,8 +78,12 @@
     
         @RequiresApi(Build.VERSION_CODES.R)
         private object Api30Impl {
    -        fun getWindowMetrics(windowManager: WindowManager): WindowMetrics {
    -            return WindowMetrics(windowManager.currentWindowMetrics.bounds)
    +        fun getWindowMetrics(
    +            windowManager: WindowManager,
    +            @UiContext context: Context
    +        ): WindowMetrics {
    +            return WindowMetrics(windowManager.currentWindowMetrics.bounds,
    +                density = context.resources.displayMetrics.density)
             }
         }
     }
    
    diff --git a/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt b/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt
    new file mode 100644
    index 0000000..0b6d904
    --- /dev/null
    +++ b/window/window-testing/src/test/java/androidx/window/testing/WindowSdkExtensionsRuleTest.kt
    
    @@ -0,0 +1,41 @@
    +/*
    + * 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 androidx.window.testing
    +
    +import androidx.window.WindowSdkExtensions
    +import org.junit.Assert.assertEquals
    +import org.junit.Rule
    +import org.junit.Test
    +
    +/** Test class to verify [WindowSdkExtensionsRule] behaviors. */
    +class WindowSdkExtensionsRuleTest {
    +
    +    @JvmField
    +    @Rule
    +    val rule = WindowSdkExtensionsRule()
    +
    +    /** Verifies the [WindowSdkExtensionsRule] behavior. */
    +    @Test
    +    fun testWindowSdkExtensionsRule() {
    +        assertEquals("The WindowSdkExtensions.extensionVersion is 0 in unit test", 0,
    +            WindowSdkExtensions.getInstance().extensionVersion)
    +
    +        rule.overrideExtensionVersion(3)
    +
    +        assertEquals(3, WindowSdkExtensions.getInstance().extensionVersion)
    +    }
    +}
    
    diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
    index ca3d9c4..d4df0a3 100644
    --- a/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
    +++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/ActivityStackTestingJavaTest.java
    
    @@ -40,8 +40,10 @@
         public void testActivityStackDefaultValue() {
             final ActivityStack activityStack = TestActivityStack.createTestActivityStack();
     
    -        assertEquals(new ActivityStack(Collections.emptyList(), false /* isEmpty */),
    -                activityStack);
    +        assertEquals(
    +                new ActivityStack(Collections.emptyList(), false /* isEmpty */),
    +                activityStack
    +        );
         }
     
         /** Verifies {@link TestActivityStack} */
    
    diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
    index 27e70c26..a797045 100644
    --- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
    +++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingJavaTest.java
    
    @@ -52,7 +52,7 @@
     public class SplitAttributesCalculatorParamsTestingJavaTest {
         private static final Rect TEST_BOUNDS = new Rect(0, 0, 2000, 2000);
         private static final WindowMetrics TEST_METRICS = new WindowMetrics(TEST_BOUNDS,
    -            WindowInsetsCompat.CONSUMED);
    +            WindowInsetsCompat.CONSUMED, 1f /* density */);
         private static final SplitAttributes DEFAULT_SPLIT_ATTRIBUTES =
                 new SplitAttributes.Builder().build();
         private static final SplitAttributes TABLETOP_HINGE_ATTRIBUTES = new SplitAttributes.Builder()
    
    diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
    index b4d1e35..57f3d9f 100644
    --- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
    +++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitAttributesCalculatorParamsTestingTest.kt
    
    @@ -102,7 +102,7 @@
     
         companion object {
             private val TEST_BOUNDS = Rect(0, 0, 2000, 2000)
    -        private val TEST_METRICS = WindowMetrics(TEST_BOUNDS)
    +        private val TEST_METRICS = WindowMetrics(TEST_BOUNDS, density = 1f)
             private val DEFAULT_SPLIT_ATTRIBUTES = SplitAttributes.Builder().build()
             private val TABLETOP_HINGE_ATTRIBUTES = SplitAttributes.Builder()
                 .setSplitType(SPLIT_TYPE_HINGE)
    
    diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
    index 5bf9eeb..106dae1 100644
    --- a/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
    +++ b/window/window-testing/src/test/java/androidx/window/testing/embedding/SplitInfoTestingTest.kt
    
    @@ -16,14 +16,12 @@
     
     package androidx.window.testing.embedding
     
    -import androidx.window.core.ExperimentalWindowApi
     import androidx.window.embedding.SplitAttributes
     import org.junit.Assert.assertEquals
     import org.junit.Test
     import org.mockito.kotlin.mock
     
     /** Test class to verify [TestSplitInfo] */
    -@OptIn(ExperimentalWindowApi::class)
     class SplitInfoTestingTest {
     
         /** Verifies the default value of [TestSplitInfo]. */
    
    diff --git a/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt b/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
    deleted file mode 100644
    index 4ccacd9..0000000
    --- a/window/window-testing/src/test/java/androidx/window/testing/embedding/TestSplitInfo.kt
    +++ /dev/null
    
    @@ -1,72 +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 androidx.window.testing.embedding
    -
    -import android.app.Activity
    -import android.os.Binder
    -import android.os.IBinder
    -import androidx.window.core.ExperimentalWindowApi
    -import androidx.window.embedding.ActivityStack
    -import androidx.window.embedding.SplitAttributes
    -import androidx.window.embedding.SplitInfo
    -
    -/**
    - * A convenience method to get a test [SplitInfo] with default values provided. With the default
    - * values it returns an empty [ActivityStack] for the primary and secondary stacks. The default
    - * [SplitAttributes] are for splitting equally and matching the locale layout.
    - *
    - * Note: This method should be used for testing local logic as opposed to end to end verification.
    - * End to end verification requires a device that supports Activity Embedding.
    - *
    - * @param primaryActivity the [Activity] for the primary container.
    - * @param secondaryActivity the [Activity] for the secondary container.
    - * @param splitAttributes the [SplitAttributes].
    - */
    -@ExperimentalWindowApi
    -fun TestSplitInfo(
    -    primaryActivity: Activity,
    -    secondaryActivity: Activity,
    -    splitAttributes: SplitAttributes = SplitAttributes(),
    -    token: IBinder = Binder()
    -): SplitInfo {
    -    val primaryActivityStack = TestActivityStack(primaryActivity, false)
    -    val secondaryActivityStack = TestActivityStack(secondaryActivity, false)
    -    return SplitInfo(primaryActivityStack, secondaryActivityStack, splitAttributes, token)
    -}
    -
    -/**
    - * A convenience method to get a test [ActivityStack] with default values provided. With the default
    - * values, there will be a single [Activity] in the stack and it will be considered not empty.
    - *
    - * Note: This method should be used for testing local logic as opposed to end to end verification.
    - * End to end verification requires a device that supports Activity Embedding.
    - *
    - * @param testActivity an [Activity] that should be considered in the stack
    - * @param isEmpty states if the stack is empty or not. In practice an [ActivityStack] with a single
    - * [Activity] but [isEmpty] set to `false` means there is an [Activity] from outside the process
    - * in the stack.
    - */
    -@ExperimentalWindowApi
    -fun TestActivityStack(
    -    testActivity: Activity,
    -    isEmpty: Boolean = true,
    -): ActivityStack {
    -    return ActivityStack(
    -        listOf(testActivity),
    -        isEmpty,
    -    )
    -}
    
    diff --git a/window/window/api/current.txt b/window/window/api/current.txt
    index f68c635..6ecbc6e 100644
    --- a/window/window/api/current.txt
    +++ b/window/window/api/current.txt
    
    @@ -13,6 +13,8 @@
         field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED";
         field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
         field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
    +    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
    +    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
       }
     
       public abstract class WindowSdkExtensions {
    @@ -107,8 +109,10 @@
     
       @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
         method public android.content.Context getContext();
    +    method public android.view.Window? getWindow();
         method public void setContentView(android.view.View view);
         property public abstract android.content.Context context;
    +    property public abstract android.view.Window? window;
       }
     
     }
    @@ -123,9 +127,13 @@
     package androidx.window.embedding {
     
       public final class ActivityEmbeddingController {
    -    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow embeddedActivityWindowInfo(android.app.Activity activity);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set activityStacks);
    +    method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
         method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
    +    method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks();
         method public boolean isActivityEmbedded(android.app.Activity activity);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration);
         field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
       }
     
    @@ -133,6 +141,11 @@
         method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
       }
     
    +  public final class ActivityEmbeddingOptions {
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public static android.os.Bundle setOverlayCreateParams(android.os.Bundle, android.app.Activity activity, androidx.window.embedding.OverlayCreateParams overlayCreateParams);
    +  }
    +
       public final class ActivityFilter {
         ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
         method public android.content.ComponentName getComponentName();
    @@ -163,6 +176,84 @@
         property public final boolean isEmpty;
       }
     
    +  public abstract class DividerAttributes {
    +    method public final int getColor();
    +    method public final int getWidthDp();
    +    property public final int color;
    +    property public final int widthDp;
    +    field public static final androidx.window.embedding.DividerAttributes.Companion Companion;
    +    field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;
    +    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
    +  }
    +
    +  public static final class DividerAttributes.Companion {
    +  }
    +
    +  public abstract static class DividerAttributes.DragRange {
    +    field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;
    +    field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;
    +  }
    +
    +  public static final class DividerAttributes.DragRange.Companion {
    +  }
    +
    +  public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {
    +    ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);
    +    method public float getMaxRatio();
    +    method public float getMinRatio();
    +    property public final float maxRatio;
    +    property public final float minRatio;
    +  }
    +
    +  public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {
    +    method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();
    +    property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;
    +  }
    +
    +  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {
    +    ctor public DividerAttributes.DraggableDividerAttributes.Builder();
    +    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
    +  }
    +
    +  public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {
    +  }
    +
    +  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {
    +    ctor public DividerAttributes.FixedDividerAttributes.Builder();
    +    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
    +  }
    +
    +  public final class EmbeddedActivityWindowInfo {
    +    method public android.graphics.Rect getBoundsInParentHost();
    +    method public android.graphics.Rect getParentHostBounds();
    +    method public boolean isEmbedded();
    +    property public final android.graphics.Rect boundsInParentHost;
    +    property public final boolean isEmbedded;
    +    property public final android.graphics.Rect parentHostBounds;
    +  }
    +
    +  public abstract class EmbeddingAnimationBackground {
    +    method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
    +    field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;
    +    field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;
    +  }
    +
    +  public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {
    +    method public int getColor();
    +    property public final int color;
    +  }
    +
    +  public static final class EmbeddingAnimationBackground.Companion {
    +    method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
    +  }
    +
       public final class EmbeddingAspectRatio {
         method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
         field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;
    @@ -174,11 +265,149 @@
         method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
       }
     
    +  public final class EmbeddingBounds {
    +    ctor public EmbeddingBounds(androidx.window.embedding.EmbeddingBounds.Alignment alignment, androidx.window.embedding.EmbeddingBounds.Dimension width, androidx.window.embedding.EmbeddingBounds.Dimension height);
    +    method public androidx.window.embedding.EmbeddingBounds.Alignment getAlignment();
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension getHeight();
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension getWidth();
    +    property public final androidx.window.embedding.EmbeddingBounds.Alignment alignment;
    +    property public final androidx.window.embedding.EmbeddingBounds.Dimension height;
    +    property public final androidx.window.embedding.EmbeddingBounds.Dimension width;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_EXPANDED;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_BOTTOM;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_LEFT;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_RIGHT;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_TOP;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Companion Companion;
    +  }
    +
    +  public static final class EmbeddingBounds.Alignment {
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_BOTTOM;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_LEFT;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_RIGHT;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_TOP;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment.Companion Companion;
    +  }
    +
    +  public static final class EmbeddingBounds.Alignment.Companion {
    +  }
    +
    +  public static final class EmbeddingBounds.Companion {
    +  }
    +
    +  public abstract static class EmbeddingBounds.Dimension {
    +    method public static final androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
    +    method public static final androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
    +    field public static final androidx.window.embedding.EmbeddingBounds.Dimension.Companion Companion;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_EXPANDED;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_HINGE;
    +  }
    +
    +  public static final class EmbeddingBounds.Dimension.Companion {
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
    +  }
    +
    +  public final class EmbeddingConfiguration {
    +    ctor public EmbeddingConfiguration();
    +    ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);
    +    method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();
    +    property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;
    +  }
    +
    +  public static final class EmbeddingConfiguration.Builder {
    +    ctor public EmbeddingConfiguration.Builder();
    +    method public androidx.window.embedding.EmbeddingConfiguration build();
    +    method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);
    +  }
    +
    +  public static final class EmbeddingConfiguration.DimAreaBehavior {
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;
    +  }
    +
    +  public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {
    +  }
    +
       public abstract class EmbeddingRule {
         method public final String? getTag();
         property public final String? tag;
       }
     
    +  public final class OverlayAttributes {
    +    ctor public OverlayAttributes();
    +    ctor public OverlayAttributes(optional androidx.window.embedding.EmbeddingBounds bounds);
    +    method public androidx.window.embedding.EmbeddingBounds getBounds();
    +    property public final androidx.window.embedding.EmbeddingBounds bounds;
    +  }
    +
    +  public static final class OverlayAttributes.Builder {
    +    ctor public OverlayAttributes.Builder();
    +    method public androidx.window.embedding.OverlayAttributes build();
    +    method public androidx.window.embedding.OverlayAttributes.Builder setBounds(androidx.window.embedding.EmbeddingBounds bounds);
    +  }
    +
    +  public final class OverlayAttributesCalculatorParams {
    +    method public androidx.window.embedding.OverlayAttributes getDefaultOverlayAttributes();
    +    method public String getOverlayTag();
    +    method public android.content.res.Configuration getParentConfiguration();
    +    method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
    +    method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
    +    property public final androidx.window.embedding.OverlayAttributes defaultOverlayAttributes;
    +    property public final String overlayTag;
    +    property public final android.content.res.Configuration parentConfiguration;
    +    property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
    +    property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
    +  }
    +
    +  public final class OverlayController {
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void clearOverlayAttributesCalculator();
    +    method public static androidx.window.embedding.OverlayController getInstance(android.content.Context context);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow overlayInfo(String overlayTag);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void setOverlayAttributesCalculator(kotlin.jvm.functions.Function1 calculator);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void updateOverlayAttributes(String overlayTag, androidx.window.embedding.OverlayAttributes overlayAttributes);
    +    field public static final androidx.window.embedding.OverlayController.Companion Companion;
    +  }
    +
    +  public static final class OverlayController.Companion {
    +    method public androidx.window.embedding.OverlayController getInstance(android.content.Context context);
    +  }
    +
    +  public final class OverlayCreateParams {
    +    ctor public OverlayCreateParams();
    +    ctor public OverlayCreateParams(optional String tag);
    +    ctor public OverlayCreateParams(optional String tag, optional androidx.window.embedding.OverlayAttributes overlayAttributes);
    +    method public static String generateOverlayTag();
    +    method public androidx.window.embedding.OverlayAttributes getOverlayAttributes();
    +    method public String getTag();
    +    property public final androidx.window.embedding.OverlayAttributes overlayAttributes;
    +    property public final String tag;
    +    field public static final androidx.window.embedding.OverlayCreateParams.Companion Companion;
    +  }
    +
    +  public static final class OverlayCreateParams.Builder {
    +    ctor public OverlayCreateParams.Builder();
    +    method public androidx.window.embedding.OverlayCreateParams build();
    +    method public androidx.window.embedding.OverlayCreateParams.Builder setOverlayAttributes(androidx.window.embedding.OverlayAttributes attrs);
    +    method public androidx.window.embedding.OverlayCreateParams.Builder setTag(String tag);
    +  }
    +
    +  public static final class OverlayCreateParams.Companion {
    +    method public String generateOverlayTag();
    +  }
    +
    +  public final class OverlayInfo {
    +    method public operator boolean contains(android.app.Activity activity);
    +    method public androidx.window.embedding.ActivityStack? getActivityStack();
    +    method public androidx.window.embedding.OverlayAttributes? getCurrentOverlayAttributes();
    +    method public String getOverlayTag();
    +    property public final androidx.window.embedding.ActivityStack? activityStack;
    +    property public final androidx.window.embedding.OverlayAttributes? currentOverlayAttributes;
    +    property public final String overlayTag;
    +  }
    +
       public final class RuleController {
         method public void addRule(androidx.window.embedding.EmbeddingRule rule);
         method public void clearRules();
    @@ -196,8 +425,17 @@
       }
     
       public final class SplitAttributes {
    +    ctor public SplitAttributes();
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.DividerAttributes dividerAttributes);
    +    method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
    +    method public androidx.window.embedding.DividerAttributes getDividerAttributes();
         method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
         method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
    +    property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
    +    property public final androidx.window.embedding.DividerAttributes dividerAttributes;
         property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
         property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
         field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
    @@ -206,6 +444,8 @@
       public static final class SplitAttributes.Builder {
         ctor public SplitAttributes.Builder();
         method public androidx.window.embedding.SplitAttributes build();
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes);
         method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
         method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
       }
    @@ -256,10 +496,11 @@
         method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
         method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
         method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
    -    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule);
         method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1 calculator);
         method public kotlinx.coroutines.flow.Flow> splitInfoList(android.app.Activity activity);
    -    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);
    +    method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
         property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
         field public static final androidx.window.embedding.SplitController.Companion Companion;
       }
    @@ -326,6 +567,24 @@
         method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
       }
     
    +  public final class SplitPinRule extends androidx.window.embedding.SplitRule {
    +    method public boolean isSticky();
    +    property public final boolean isSticky;
    +  }
    +
    +  public static final class SplitPinRule.Builder {
    +    ctor public SplitPinRule.Builder();
    +    method public androidx.window.embedding.SplitPinRule build();
    +    method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
    +    method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);
    +    method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);
    +  }
    +
       public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
         method public java.util.Set getFilters();
         method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
    @@ -431,10 +690,20 @@
       public static final class FoldingFeature.State.Companion {
       }
     
    +  public final class SupportedPosture {
    +    field public static final androidx.window.layout.SupportedPosture.Companion Companion;
    +    field public static final androidx.window.layout.SupportedPosture TABLETOP;
    +  }
    +
    +  public static final class SupportedPosture.Companion {
    +  }
    +
       public interface WindowInfoTracker {
         method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
    +    method public default java.util.List getSupportedPostures();
         method public kotlinx.coroutines.flow.Flow windowLayoutInfo(android.app.Activity activity);
         method public default kotlinx.coroutines.flow.Flow windowLayoutInfo(@UiContext android.content.Context context);
    +    property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List supportedPostures;
         field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
       }
     
    @@ -449,8 +718,10 @@
     
       public final class WindowMetrics {
         method public android.graphics.Rect getBounds();
    +    method public float getDensity();
         method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets();
         property public final android.graphics.Rect bounds;
    +    property public final float density;
       }
     
       public interface WindowMetricsCalculator {
    
    diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
    index f68c635..6ecbc6e 100644
    --- a/window/window/api/restricted_current.txt
    +++ b/window/window/api/restricted_current.txt
    
    @@ -13,6 +13,8 @@
         field public static final String PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED = "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED";
         field public static final String PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE";
         field public static final String PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES = "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES";
    +    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
    +    field public static final String PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE = "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
       }
     
       public abstract class WindowSdkExtensions {
    @@ -107,8 +109,10 @@
     
       @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionPresenter extends androidx.window.area.WindowAreaSession {
         method public android.content.Context getContext();
    +    method public android.view.Window? getWindow();
         method public void setContentView(android.view.View view);
         property public abstract android.content.Context context;
    +    property public abstract android.view.Window? window;
       }
     
     }
    @@ -123,9 +127,13 @@
     package androidx.window.embedding {
     
       public final class ActivityEmbeddingController {
    -    method @SuppressCompatibility @androidx.window.core.ExperimentalWindowApi public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow embeddedActivityWindowInfo(android.app.Activity activity);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void finishActivityStacks(java.util.Set activityStacks);
    +    method public androidx.window.embedding.ActivityStack? getActivityStack(android.app.Activity activity);
         method public static androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
    +    method @androidx.window.RequiresWindowSdkExtension(version=3) public void invalidateVisibleActivityStacks();
         method public boolean isActivityEmbedded(android.app.Activity activity);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void setEmbeddingConfiguration(androidx.window.embedding.EmbeddingConfiguration embeddingConfiguration);
         field public static final androidx.window.embedding.ActivityEmbeddingController.Companion Companion;
       }
     
    @@ -133,6 +141,11 @@
         method public androidx.window.embedding.ActivityEmbeddingController getInstance(android.content.Context context);
       }
     
    +  public final class ActivityEmbeddingOptions {
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public static android.os.Bundle setLaunchingActivityStack(android.os.Bundle, android.content.Context context, androidx.window.embedding.ActivityStack activityStack);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public static android.os.Bundle setOverlayCreateParams(android.os.Bundle, android.app.Activity activity, androidx.window.embedding.OverlayCreateParams overlayCreateParams);
    +  }
    +
       public final class ActivityFilter {
         ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
         method public android.content.ComponentName getComponentName();
    @@ -163,6 +176,84 @@
         property public final boolean isEmpty;
       }
     
    +  public abstract class DividerAttributes {
    +    method public final int getColor();
    +    method public final int getWidthDp();
    +    property public final int color;
    +    property public final int widthDp;
    +    field public static final androidx.window.embedding.DividerAttributes.Companion Companion;
    +    field public static final androidx.window.embedding.DividerAttributes NO_DIVIDER;
    +    field public static final int WIDTH_SYSTEM_DEFAULT = -1; // 0xffffffff
    +  }
    +
    +  public static final class DividerAttributes.Companion {
    +  }
    +
    +  public abstract static class DividerAttributes.DragRange {
    +    field public static final androidx.window.embedding.DividerAttributes.DragRange.Companion Companion;
    +    field public static final androidx.window.embedding.DividerAttributes.DragRange DRAG_RANGE_SYSTEM_DEFAULT;
    +  }
    +
    +  public static final class DividerAttributes.DragRange.Companion {
    +  }
    +
    +  public static final class DividerAttributes.DragRange.SplitRatioDragRange extends androidx.window.embedding.DividerAttributes.DragRange {
    +    ctor public DividerAttributes.DragRange.SplitRatioDragRange(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float minRatio, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float maxRatio);
    +    method public float getMaxRatio();
    +    method public float getMinRatio();
    +    property public final float maxRatio;
    +    property public final float minRatio;
    +  }
    +
    +  public static final class DividerAttributes.DraggableDividerAttributes extends androidx.window.embedding.DividerAttributes {
    +    method public androidx.window.embedding.DividerAttributes.DragRange getDragRange();
    +    property public final androidx.window.embedding.DividerAttributes.DragRange dragRange;
    +  }
    +
    +  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.DraggableDividerAttributes.Builder {
    +    ctor public DividerAttributes.DraggableDividerAttributes.Builder();
    +    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.DraggableDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.DraggableDividerAttributes original);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes build();
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setColor(@ColorInt int color);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setDragRange(androidx.window.embedding.DividerAttributes.DragRange dragRange);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.DraggableDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
    +  }
    +
    +  public static final class DividerAttributes.FixedDividerAttributes extends androidx.window.embedding.DividerAttributes {
    +  }
    +
    +  @androidx.window.RequiresWindowSdkExtension(version=6) public static final class DividerAttributes.FixedDividerAttributes.Builder {
    +    ctor public DividerAttributes.FixedDividerAttributes.Builder();
    +    ctor @androidx.window.RequiresWindowSdkExtension(version=6) public DividerAttributes.FixedDividerAttributes.Builder(androidx.window.embedding.DividerAttributes.FixedDividerAttributes original);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes build();
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setColor(@ColorInt int color);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.DividerAttributes.FixedDividerAttributes.Builder setWidthDp(@IntRange(from=androidx.window.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT.toLong()) int widthDp);
    +  }
    +
    +  public final class EmbeddedActivityWindowInfo {
    +    method public android.graphics.Rect getBoundsInParentHost();
    +    method public android.graphics.Rect getParentHostBounds();
    +    method public boolean isEmbedded();
    +    property public final android.graphics.Rect boundsInParentHost;
    +    property public final boolean isEmbedded;
    +    property public final android.graphics.Rect parentHostBounds;
    +  }
    +
    +  public abstract class EmbeddingAnimationBackground {
    +    method public static final androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
    +    field public static final androidx.window.embedding.EmbeddingAnimationBackground.Companion Companion;
    +    field public static final androidx.window.embedding.EmbeddingAnimationBackground DEFAULT;
    +  }
    +
    +  public static final class EmbeddingAnimationBackground.ColorBackground extends androidx.window.embedding.EmbeddingAnimationBackground {
    +    method public int getColor();
    +    property public final int color;
    +  }
    +
    +  public static final class EmbeddingAnimationBackground.Companion {
    +    method public androidx.window.embedding.EmbeddingAnimationBackground.ColorBackground createColorBackground(@ColorInt @IntRange(from=android.graphics.Color.BLACK.toLong(), to=android.graphics.Color.WHITE.toLong()) int color);
    +  }
    +
       public final class EmbeddingAspectRatio {
         method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
         field public static final androidx.window.embedding.EmbeddingAspectRatio ALWAYS_ALLOW;
    @@ -174,11 +265,149 @@
         method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
       }
     
    +  public final class EmbeddingBounds {
    +    ctor public EmbeddingBounds(androidx.window.embedding.EmbeddingBounds.Alignment alignment, androidx.window.embedding.EmbeddingBounds.Dimension width, androidx.window.embedding.EmbeddingBounds.Dimension height);
    +    method public androidx.window.embedding.EmbeddingBounds.Alignment getAlignment();
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension getHeight();
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension getWidth();
    +    property public final androidx.window.embedding.EmbeddingBounds.Alignment alignment;
    +    property public final androidx.window.embedding.EmbeddingBounds.Dimension height;
    +    property public final androidx.window.embedding.EmbeddingBounds.Dimension width;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_EXPANDED;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_BOTTOM;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_LEFT;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_RIGHT;
    +    field public static final androidx.window.embedding.EmbeddingBounds BOUNDS_HINGE_TOP;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Companion Companion;
    +  }
    +
    +  public static final class EmbeddingBounds.Alignment {
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_BOTTOM;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_LEFT;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_RIGHT;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment ALIGN_TOP;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Alignment.Companion Companion;
    +  }
    +
    +  public static final class EmbeddingBounds.Alignment.Companion {
    +  }
    +
    +  public static final class EmbeddingBounds.Companion {
    +  }
    +
    +  public abstract static class EmbeddingBounds.Dimension {
    +    method public static final androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
    +    method public static final androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
    +    field public static final androidx.window.embedding.EmbeddingBounds.Dimension.Companion Companion;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_EXPANDED;
    +    field public static final androidx.window.embedding.EmbeddingBounds.Dimension DIMENSION_HINGE;
    +  }
    +
    +  public static final class EmbeddingBounds.Dimension.Companion {
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension pixel(@IntRange(from=1L) @Px int value);
    +    method public androidx.window.embedding.EmbeddingBounds.Dimension ratio(@FloatRange(from=0.0, fromInclusive=false, to=1.0, toInclusive=false) float ratio);
    +  }
    +
    +  public final class EmbeddingConfiguration {
    +    ctor public EmbeddingConfiguration();
    +    ctor public EmbeddingConfiguration(optional @androidx.window.RequiresWindowSdkExtension(version=5) androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior);
    +    method public androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior getDimAreaBehavior();
    +    property public final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior dimAreaBehavior;
    +  }
    +
    +  public static final class EmbeddingConfiguration.Builder {
    +    ctor public EmbeddingConfiguration.Builder();
    +    method public androidx.window.embedding.EmbeddingConfiguration build();
    +    method public androidx.window.embedding.EmbeddingConfiguration.Builder setDimAreaBehavior(androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior area);
    +  }
    +
    +  public static final class EmbeddingConfiguration.DimAreaBehavior {
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion Companion;
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_ACTIVITY_STACK;
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior ON_TASK;
    +    field public static final androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior UNDEFINED;
    +  }
    +
    +  public static final class EmbeddingConfiguration.DimAreaBehavior.Companion {
    +  }
    +
       public abstract class EmbeddingRule {
         method public final String? getTag();
         property public final String? tag;
       }
     
    +  public final class OverlayAttributes {
    +    ctor public OverlayAttributes();
    +    ctor public OverlayAttributes(optional androidx.window.embedding.EmbeddingBounds bounds);
    +    method public androidx.window.embedding.EmbeddingBounds getBounds();
    +    property public final androidx.window.embedding.EmbeddingBounds bounds;
    +  }
    +
    +  public static final class OverlayAttributes.Builder {
    +    ctor public OverlayAttributes.Builder();
    +    method public androidx.window.embedding.OverlayAttributes build();
    +    method public androidx.window.embedding.OverlayAttributes.Builder setBounds(androidx.window.embedding.EmbeddingBounds bounds);
    +  }
    +
    +  public final class OverlayAttributesCalculatorParams {
    +    method public androidx.window.embedding.OverlayAttributes getDefaultOverlayAttributes();
    +    method public String getOverlayTag();
    +    method public android.content.res.Configuration getParentConfiguration();
    +    method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
    +    method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
    +    property public final androidx.window.embedding.OverlayAttributes defaultOverlayAttributes;
    +    property public final String overlayTag;
    +    property public final android.content.res.Configuration parentConfiguration;
    +    property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
    +    property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
    +  }
    +
    +  public final class OverlayController {
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void clearOverlayAttributesCalculator();
    +    method public static androidx.window.embedding.OverlayController getInstance(android.content.Context context);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public kotlinx.coroutines.flow.Flow overlayInfo(String overlayTag);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void setOverlayAttributesCalculator(kotlin.jvm.functions.Function1 calculator);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public void updateOverlayAttributes(String overlayTag, androidx.window.embedding.OverlayAttributes overlayAttributes);
    +    field public static final androidx.window.embedding.OverlayController.Companion Companion;
    +  }
    +
    +  public static final class OverlayController.Companion {
    +    method public androidx.window.embedding.OverlayController getInstance(android.content.Context context);
    +  }
    +
    +  public final class OverlayCreateParams {
    +    ctor public OverlayCreateParams();
    +    ctor public OverlayCreateParams(optional String tag);
    +    ctor public OverlayCreateParams(optional String tag, optional androidx.window.embedding.OverlayAttributes overlayAttributes);
    +    method public static String generateOverlayTag();
    +    method public androidx.window.embedding.OverlayAttributes getOverlayAttributes();
    +    method public String getTag();
    +    property public final androidx.window.embedding.OverlayAttributes overlayAttributes;
    +    property public final String tag;
    +    field public static final androidx.window.embedding.OverlayCreateParams.Companion Companion;
    +  }
    +
    +  public static final class OverlayCreateParams.Builder {
    +    ctor public OverlayCreateParams.Builder();
    +    method public androidx.window.embedding.OverlayCreateParams build();
    +    method public androidx.window.embedding.OverlayCreateParams.Builder setOverlayAttributes(androidx.window.embedding.OverlayAttributes attrs);
    +    method public androidx.window.embedding.OverlayCreateParams.Builder setTag(String tag);
    +  }
    +
    +  public static final class OverlayCreateParams.Companion {
    +    method public String generateOverlayTag();
    +  }
    +
    +  public final class OverlayInfo {
    +    method public operator boolean contains(android.app.Activity activity);
    +    method public androidx.window.embedding.ActivityStack? getActivityStack();
    +    method public androidx.window.embedding.OverlayAttributes? getCurrentOverlayAttributes();
    +    method public String getOverlayTag();
    +    property public final androidx.window.embedding.ActivityStack? activityStack;
    +    property public final androidx.window.embedding.OverlayAttributes? currentOverlayAttributes;
    +    property public final String overlayTag;
    +  }
    +
       public final class RuleController {
         method public void addRule(androidx.window.embedding.EmbeddingRule rule);
         method public void clearRules();
    @@ -196,8 +425,17 @@
       }
     
       public final class SplitAttributes {
    +    ctor public SplitAttributes();
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType);
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground);
    +    ctor public SplitAttributes(optional androidx.window.embedding.SplitAttributes.SplitType splitType, optional androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection, optional androidx.window.embedding.EmbeddingAnimationBackground animationBackground, optional androidx.window.embedding.DividerAttributes dividerAttributes);
    +    method public androidx.window.embedding.EmbeddingAnimationBackground getAnimationBackground();
    +    method public androidx.window.embedding.DividerAttributes getDividerAttributes();
         method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
         method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
    +    property public final androidx.window.embedding.EmbeddingAnimationBackground animationBackground;
    +    property public final androidx.window.embedding.DividerAttributes dividerAttributes;
         property public final androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection;
         property public final androidx.window.embedding.SplitAttributes.SplitType splitType;
         field public static final androidx.window.embedding.SplitAttributes.Companion Companion;
    @@ -206,6 +444,8 @@
       public static final class SplitAttributes.Builder {
         ctor public SplitAttributes.Builder();
         method public androidx.window.embedding.SplitAttributes build();
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public androidx.window.embedding.SplitAttributes.Builder setAnimationBackground(androidx.window.embedding.EmbeddingAnimationBackground background);
    +    method @androidx.window.RequiresWindowSdkExtension(version=6) public androidx.window.embedding.SplitAttributes.Builder setDividerAttributes(androidx.window.embedding.DividerAttributes dividerAttributes);
         method public androidx.window.embedding.SplitAttributes.Builder setLayoutDirection(androidx.window.embedding.SplitAttributes.LayoutDirection layoutDirection);
         method public androidx.window.embedding.SplitAttributes.Builder setSplitType(androidx.window.embedding.SplitAttributes.SplitType type);
       }
    @@ -256,10 +496,11 @@
         method @androidx.window.RequiresWindowSdkExtension(version=2) public void clearSplitAttributesCalculator();
         method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
         method public androidx.window.embedding.SplitController.SplitSupportStatus getSplitSupportStatus();
    -    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void invalidateTopVisibleSplitAttributes();
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public boolean pinTopActivityStack(int taskId, androidx.window.embedding.SplitPinRule splitPinRule);
         method @androidx.window.RequiresWindowSdkExtension(version=2) public void setSplitAttributesCalculator(kotlin.jvm.functions.Function1 calculator);
         method public kotlinx.coroutines.flow.Flow> splitInfoList(android.app.Activity activity);
    -    method @SuppressCompatibility @androidx.window.RequiresWindowSdkExtension(version=3) @androidx.window.core.ExperimentalWindowApi public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
    +    method @androidx.window.RequiresWindowSdkExtension(version=5) public void unpinTopActivityStack(int taskId);
    +    method @androidx.window.RequiresWindowSdkExtension(version=3) public void updateSplitAttributes(androidx.window.embedding.SplitInfo splitInfo, androidx.window.embedding.SplitAttributes splitAttributes);
         property public final androidx.window.embedding.SplitController.SplitSupportStatus splitSupportStatus;
         field public static final androidx.window.embedding.SplitController.Companion Companion;
       }
    @@ -326,6 +567,24 @@
         method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
       }
     
    +  public final class SplitPinRule extends androidx.window.embedding.SplitRule {
    +    method public boolean isSticky();
    +    property public final boolean isSticky;
    +  }
    +
    +  public static final class SplitPinRule.Builder {
    +    ctor public SplitPinRule.Builder();
    +    method public androidx.window.embedding.SplitPinRule build();
    +    method public androidx.window.embedding.SplitPinRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
    +    method public androidx.window.embedding.SplitPinRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
    +    method public androidx.window.embedding.SplitPinRule.Builder setSticky(boolean isSticky);
    +    method public androidx.window.embedding.SplitPinRule.Builder setTag(String? tag);
    +  }
    +
       public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
         method public java.util.Set getFilters();
         method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
    @@ -431,10 +690,20 @@
       public static final class FoldingFeature.State.Companion {
       }
     
    +  public final class SupportedPosture {
    +    field public static final androidx.window.layout.SupportedPosture.Companion Companion;
    +    field public static final androidx.window.layout.SupportedPosture TABLETOP;
    +  }
    +
    +  public static final class SupportedPosture.Companion {
    +  }
    +
       public interface WindowInfoTracker {
         method public static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
    +    method public default java.util.List getSupportedPostures();
         method public kotlinx.coroutines.flow.Flow windowLayoutInfo(android.app.Activity activity);
         method public default kotlinx.coroutines.flow.Flow windowLayoutInfo(@UiContext android.content.Context context);
    +    property @androidx.window.RequiresWindowSdkExtension(version=6) public default java.util.List supportedPostures;
         field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
       }
     
    @@ -449,8 +718,10 @@
     
       public final class WindowMetrics {
         method public android.graphics.Rect getBounds();
    +    method public float getDensity();
         method @SuppressCompatibility @RequiresApi(android.os.Build.VERSION_CODES.R) @androidx.window.core.ExperimentalWindowApi public androidx.core.view.WindowInsetsCompat getWindowInsets();
         property public final android.graphics.Rect bounds;
    +    property public final float density;
       }
     
       public interface WindowMetricsCalculator {
    
    diff --git a/window/window/build.gradle b/window/window/build.gradle
    index 076e5af..3c9306c6 100644
    --- a/window/window/build.gradle
    +++ b/window/window/build.gradle
    
    @@ -57,7 +57,7 @@
         implementation("androidx.core:core:1.8.0")
     
         def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
    -    def extensions_version = "androidx.window.extensions:extensions:1.2.0"
    +    def extensions_version = project(":window:extensions:extensions")
         // A compile only dependency on extnensions.core so that other libraries do not expose it
         // transitively.
         compileOnly(extensions_core_version)
    @@ -82,7 +82,6 @@
         testImplementation(libs.kotlinCoroutinesTest)
         testImplementation(extensions_version)
         testImplementation(compileOnly(project(":window:sidecar:sidecar")))
    -    testImplementation(compileOnly(extensions_version))
     
         androidTestImplementation(libs.testCore)
         androidTestImplementation(libs.kotlinTestJunit)
    
    diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
    new file mode 100644
    index 0000000..a8de90b
    --- /dev/null
    +++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/FinishActivityStacksSamples.kt
    
    @@ -0,0 +1,37 @@
    +/*
    + * 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 androidx.window.samples.embedding
    +
    +import android.app.Activity
    +import androidx.annotation.Sampled
    +import androidx.window.embedding.ActivityEmbeddingController
    +import androidx.window.embedding.SplitController
    +
    +@Sampled
    +suspend fun expandPrimaryContainer() {
    +    SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity)
    +        .collect { splitInfoList ->
    +            // Find all associated secondary ActivityStacks
    +            val associatedSecondaryActivityStacks = splitInfoList
    +                .mapTo(mutableSetOf()) { splitInfo -> splitInfo.secondaryActivityStack }
    +            // Finish them all.
    +            ActivityEmbeddingController.getInstance(primaryActivity)
    +                .finishActivityStacks(associatedSecondaryActivityStacks)
    +        }
    +}
    +
    +val primaryActivity = Activity()
    
    diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt
    new file mode 100644
    index 0000000..c9c3857
    --- /dev/null
    +++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/LaunchingActivityStackSample.kt
    
    @@ -0,0 +1,66 @@
    +/*
    + * 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 androidx.window.samples.embedding
    +
    +import android.app.Activity
    +import androidx.annotation.Sampled
    +import androidx.core.app.ActivityOptionsCompat
    +import androidx.window.embedding.ActivityStack
    +import androidx.window.embedding.OverlayController
    +import androidx.window.embedding.SplitController
    +import androidx.window.embedding.setLaunchingActivityStack
    +
    +@Sampled
    +suspend fun launchingOnPrimaryActivityStack() {
    +    var primaryActivityStack: ActivityStack? = null
    +
    +    SplitController.getInstance(primaryActivity).splitInfoList(primaryActivity)
    +        .collect { splitInfoList ->
    +            primaryActivityStack = splitInfoList.last().primaryActivityStack
    +        }
    +
    +    primaryActivity.startActivity(
    +        INTENT,
    +        ActivityOptionsCompat.makeBasic().toBundle()!!.setLaunchingActivityStack(
    +            primaryActivity,
    +            primaryActivityStack!!
    +        )
    +    )
    +}
    +
    +@Sampled
    +suspend fun launchingOnOverlayActivityStack() {
    +    var overlayActivityStack: ActivityStack? = null
    +
    +    OverlayController.getInstance(context).overlayInfo(TAG_OVERLAY).collect { overlayInfo ->
    +        overlayActivityStack = overlayInfo.activityStack
    +    }
    +
    +    // The use case is to launch an Activity to an existing overlay ActivityStack from the overlain
    +    // Activity. If activityStack is not specified, the activity is launched to the top of the
    +    // host task behind the overlay ActivityStack.
    +    overlainActivity.startActivity(
    +        INTENT,
    +        ActivityOptionsCompat.makeBasic().toBundle()!!.setLaunchingActivityStack(
    +            overlainActivity,
    +            overlayActivityStack!!
    +        )
    +    )
    +}
    +
    +const val TAG_OVERLAY = "overlay"
    +val overlainActivity = Activity()
    
    diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt
    new file mode 100644
    index 0000000..9a04029
    --- /dev/null
    +++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/OverlaySamples.kt
    
    @@ -0,0 +1,79 @@
    +/*
    + * 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 androidx.window.samples.embedding
    +
    +import android.app.Activity
    +import android.content.Intent
    +import android.graphics.Rect
    +import androidx.annotation.Sampled
    +import androidx.core.app.ActivityOptionsCompat
    +import androidx.window.embedding.EmbeddingBounds
    +import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio
    +import androidx.window.embedding.OverlayAttributes
    +import androidx.window.embedding.OverlayController
    +import androidx.window.embedding.OverlayCreateParams
    +import androidx.window.embedding.setOverlayCreateParams
    +
    +@Sampled
    +fun launchOverlayActivityStackSample() {
    +    // Creates an overlay container on the right
    +    val params = OverlayCreateParams(
    +        overlayAttributes = OverlayAttributes(
    +            EmbeddingBounds(
    +                alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,
    +                width = ratio(0.5f),
    +                height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
    +            )
    +        )
    +    )
    +
    +    val optionsWithOverlayParams = ActivityOptionsCompat.makeBasic().toBundle()
    +        ?.setOverlayCreateParams(launchingActivity, params)
    +
    +    // Start INTENT to the overlay container specified by params.
    +    launchingActivity.startActivity(INTENT, optionsWithOverlayParams)
    +}
    +
    +@Sampled
    +fun overlayAttributesCalculatorSample() {
    +    // A sample to show overlay on the bottom if the device is portrait, and on the right when
    +    // the device is landscape.
    +    OverlayController.getInstance(launchingActivity).setOverlayAttributesCalculator { params ->
    +        val taskBounds = params.parentWindowMetrics.bounds
    +        return@setOverlayAttributesCalculator OverlayAttributes(
    +            if (taskBounds.isPortrait()) {
    +                EmbeddingBounds(
    +                    alignment = EmbeddingBounds.Alignment.ALIGN_BOTTOM,
    +                    width = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
    +                    height = ratio(0.5f),
    +                )
    +            } else {
    +                EmbeddingBounds(
    +                    alignment = EmbeddingBounds.Alignment.ALIGN_RIGHT,
    +                    width = ratio(0.5f),
    +                    height = EmbeddingBounds.Dimension.DIMENSION_EXPANDED,
    +                )
    +            }
    +        )
    +    }
    +}
    +
    +private fun Rect.isPortrait(): Boolean = height() >= width()
    +
    +val launchingActivity: Activity = Activity()
    +
    +val INTENT = Intent()
    
    diff --git a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
    index d1ac513..e7e7a53 100644
    --- a/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
    +++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
    
    @@ -17,7 +17,9 @@
     package androidx.window.samples.embedding
     
     import android.app.Application
    +import android.graphics.Color
     import androidx.annotation.Sampled
    +import androidx.window.embedding.EmbeddingAnimationBackground
     import androidx.window.embedding.SplitAttributes
     import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EQUAL
     import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_EXPAND
    @@ -58,6 +60,10 @@
                                 SplitAttributes.LayoutDirection.LOCALE
                             }
                         )
    +                    // Optionally set the animation background to use when switching between
    +                    // vertical and horizontal
    +                    .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(
    +                        Color.GRAY))
                         .build()
                 }
                 return@setSplitAttributesCalculator if (
    @@ -67,6 +73,10 @@
                     SplitAttributes.Builder()
                         .setSplitType(SPLIT_TYPE_EQUAL)
                         .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    +                    // Optionally set the animation background to use when switching between
    +                    // vertical and horizontal
    +                    .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(
    +                        Color.GRAY))
                         .build()
                 } else {
                     // Expand containers if the device is in portrait or the width is less than 600 dp.
    @@ -88,12 +98,18 @@
                 return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 600) {
                     builder
                         .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    -                    // Set the color to use when switching between vertical and horizontal
    +                    // Optionally set the animation background to use when switching between
    +                    // vertical and horizontal
    +                    .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(
    +                        Color.GRAY))
                         .build()
                 } else if (parentConfiguration.screenHeightDp >= 600) {
                     builder
                         .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
    -                    // Set the color to use when switching between vertical and horizontal
    +                    // Optionally set the animation background to use when switching between
    +                    // vertical and horizontal
    +                    .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(
    +                        Color.GRAY))
                         .build()
                 } else {
                     // Fallback to expand the secondary container
    
    diff --git a/window/window/src/androidTest/AndroidManifest.xml b/window/window/src/androidTest/AndroidManifest.xml
    index e0d60f16..2393bc6 100644
    --- a/window/window/src/androidTest/AndroidManifest.xml
    +++ b/window/window/src/androidTest/AndroidManifest.xml
    
    @@ -42,5 +42,13 @@
                 android:name=
                     "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
                 android:value="false" />
    +        
    +            android:name=
    +                "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
    +            android:value="false" />
    +        
    +            android:name=
    +                "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
    +            android:value="false" />
         
     
    
    diff --git a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
    index ca75428..41e754e 100644
    --- a/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/WindowPropertiesTest.kt
    
    @@ -124,6 +124,42 @@
             }
         }
     
    +    @Test
    +    fun test_property_allow_user_aspect_ratio_override() {
    +        assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    +            // No-op, but to suppress lint
    +            return
    +        }
    +        activityRule.scenario.onActivity { activity ->
    +            // Should be false as defined in AndroidManifest.xml
    +            assertFalse(
    +                getProperty(
    +                    activity,
    +                    WindowProperties.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE
    +                )
    +            )
    +        }
    +    }
    +
    +    @Test
    +    fun test_property_allow_user_aspect_ratio_fullscreen_override() {
    +        assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    +            // No-op, but to suppress lint
    +            return
    +        }
    +        activityRule.scenario.onActivity { activity ->
    +            // Should be false as defined in AndroidManifest.xml
    +            assertFalse(
    +                getProperty(
    +                    activity,
    +                    WindowProperties.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE
    +                )
    +            )
    +        }
    +    }
    +
         @RequiresApi(Build.VERSION_CODES.S)
         @Throws(PackageManager.NameNotFoundException::class)
         private fun getProperty(context: Context, propertyName: String): Boolean {
    
    diff --git a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
    index 96060ea..2eadc45 100644
    --- a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
    +++ b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
    
    @@ -1,5 +1,6 @@
     package androidx.window
     
    +import android.app.Activity
     import android.app.Application
     import android.content.Context
     import android.hardware.display.DisplayManager
    @@ -7,7 +8,10 @@
     import android.view.Display
     import android.view.WindowManager
     import androidx.annotation.RequiresApi
    +import androidx.lifecycle.Lifecycle
    +import androidx.test.core.app.ActivityScenario
     import androidx.test.core.app.ApplicationProvider
    +import androidx.test.ext.junit.rules.ActivityScenarioRule
     import org.junit.Assume.assumeTrue
     
     open class WindowTestUtils {
    @@ -25,17 +29,59 @@
                 )
             }
     
    -        @OptIn(androidx.window.core.ExperimentalWindowApi::class)
             fun assumeAtLeastVendorApiLevel(min: Int) {
    -            val version = WindowSdkExtensions.getInstance().extensionVersion
    -            assumeTrue(version >= min)
    +            val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
    +            assumeTrue(apiLevel >= min)
             }
     
    -        @OptIn(androidx.window.core.ExperimentalWindowApi::class)
             fun assumeBeforeVendorApiLevel(max: Int) {
    -            val version = WindowSdkExtensions.getInstance().extensionVersion
    -            assumeTrue(version < max)
    -            assumeTrue(version > 0)
    +            val apiLevel = WindowSdkExtensions.getInstance().extensionVersion
    +            assumeTrue(apiLevel < max)
    +            assumeTrue(apiLevel > 0)
    +        }
    +
    +        fun isInMultiWindowMode(activity: Activity): Boolean {
    +            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    +                activity.isInMultiWindowMode
    +            } else false
    +        }
    +
    +        fun assumePlatformBeforeR() {
    +            assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
    +        }
    +
    +        fun assumePlatformROrAbove() {
    +            assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
    +        }
    +
    +        fun assumePlatformBeforeU() {
    +            assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        }
    +
    +        fun assumePlatformUOrAbove() {
    +            assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +        }
    +
    +        /**
    +         * Creates and launches an activity performing the supplied actions at various points in the
    +         * activity lifecycle.
    +         *
    +         * @param initialAction the action that will run once before the activity is created.
    +         * @param verifyAction the action to run once after each change in activity lifecycle state.
    +         */
    +        fun runActionsAcrossActivityLifecycle(
    +            scenarioRule: ActivityScenarioRule,
    +            initialAction: ActivityScenario.ActivityAction,
    +            verifyAction: ActivityScenario.ActivityAction
    +        ) {
    +            val scenario = scenarioRule.scenario
    +            scenario.onActivity(initialAction)
    +            scenario.moveToState(Lifecycle.State.CREATED)
    +            scenario.onActivity(verifyAction)
    +            scenario.moveToState(Lifecycle.State.STARTED)
    +            scenario.onActivity(verifyAction)
    +            scenario.moveToState(Lifecycle.State.RESUMED)
    +            scenario.onActivity(verifyAction)
             }
         }
     }
    
    diff --git a/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
    index ccf3521..b7e6c9b 100644
    --- a/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/area/SafeWindowAreaComponentProviderTest.kt
    
    @@ -18,6 +18,7 @@
     
     import androidx.window.core.ExtensionsUtil
     import androidx.window.extensions.WindowExtensionsProvider
    +import kotlin.test.assertNotNull
     import org.junit.Assert.assertNull
     import org.junit.Assume.assumeTrue
     import org.junit.Test
    @@ -42,8 +43,8 @@
             try {
                 val extensions = WindowExtensionsProvider.getWindowExtensions()
                 val actualComponent = extensions.windowAreaComponent
    -            if (actualComponent == null) {
    -                assertNull(safeComponent)
    +            if (actualComponent != null) {
    +                assertNotNull(safeComponent)
                 }
                 // TODO(b/267831038): verify upon each api level
                 // TODO(b/267708462): more reliable test for testing actual method matching
    
    diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
    index 341bcfe..a480984 100644
    --- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
    
    @@ -23,6 +23,7 @@
     import android.os.Build
     import android.util.DisplayMetrics
     import android.view.View
    +import android.view.Window
     import android.widget.TextView
     import androidx.annotation.RequiresApi
     import androidx.test.ext.junit.rules.ActivityScenarioRule
    @@ -85,7 +86,7 @@
                 val extensionComponent = FakeWindowAreaComponent()
                 val controller = WindowAreaControllerImpl(
                     windowAreaComponent = extensionComponent,
    -                vendorApiLevel = FEATURE_VENDOR_API_LEVEL
    +                presentationSupported = true
                 )
                 extensionComponent.currentRearDisplayStatus = STATUS_UNAVAILABLE
                 extensionComponent.currentRearDisplayPresentationStatus = STATUS_UNAVAILABLE
    @@ -171,7 +172,7 @@
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = FEATURE_VENDOR_API_LEVEL
    +            presentationSupported = true
             )
             extensions.currentRearDisplayStatus = STATUS_AVAILABLE
             val callback = TestWindowAreaSessionCallback()
    @@ -235,7 +236,7 @@
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = FEATURE_VENDOR_API_LEVEL
    +            presentationSupported = true
             )
             extensions.currentRearDisplayStatus = initialState
             val callback = TestWindowAreaSessionCallback()
    @@ -278,7 +279,7 @@
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = FEATURE_VENDOR_API_LEVEL
    +            presentationSupported = true
             )
     
             extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
    @@ -328,7 +329,7 @@
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = 3
    +            presentationSupported = true
             )
     
             extensions.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
    @@ -349,7 +350,7 @@
             // Create a new controller to start the presentation.
             val controller2 = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = 3
    +            presentationSupported = true
             )
     
             val callback = TestWindowAreaPresentationSessionCallback()
    @@ -385,7 +386,7 @@
             val extensions = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = 3
    +            presentationSupported = true
             )
             extensions.currentRearDisplayStatus = STATUS_AVAILABLE
             val callback = TestWindowAreaSessionCallback()
    @@ -409,7 +410,7 @@
             // Create a new controller to start the transfer.
             val controller2 = WindowAreaControllerImpl(
                 windowAreaComponent = extensions,
    -            vendorApiLevel = 3
    +            presentationSupported = true
             )
     
             activityScenario.scenario.onActivity { testActivity ->
    @@ -442,7 +443,7 @@
             val extensionComponent = FakeWindowAreaComponent()
             val controller = WindowAreaControllerImpl(
                 windowAreaComponent = extensionComponent,
    -            vendorApiLevel = FEATURE_VENDOR_API_LEVEL
    +            presentationSupported = true
             )
     
             extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
    @@ -646,11 +647,13 @@
             override fun setPresentationView(view: View) {
                 sessionConsumer.accept(WindowAreaComponent.SESSION_STATE_CONTENT_VISIBLE)
             }
    +
    +        override fun getWindow(): Window {
    +            return activity.window
    +        }
         }
     
         companion object {
             private const val REAR_FACING_BINDER_DESCRIPTION = "TEST_WINDOW_AREA_REAR_FACING"
    -
    -        private const val FEATURE_VENDOR_API_LEVEL = 3
         }
     }
    
    diff --git a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
    index 532fd65..4798785 100644
    --- a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
    
    @@ -46,6 +46,15 @@
                     WindowAreaComponentFullImplementation::class.java, 3))
         }
     
    +    @Test
    +    fun isWindowAreaComponentValid_apiLevel1() {
    +        assertFalse(
    +            WindowAreaComponentValidator.isWindowAreaComponentValid(
    +                WindowAreaComponentApiV2Implementation::class.java, apiLevel = 1
    +            )
    +        )
    +    }
    +
         /**
          * Test that validator returns correct results for API Level 2 [WindowAreaComponent]
          * implementation.
    @@ -54,10 +63,15 @@
         fun isWindowAreaComponentValid_apiLevel2() {
             assertTrue(
                 WindowAreaComponentValidator.isWindowAreaComponentValid(
    -                WindowAreaComponentApiV2Implementation::class.java, 2))
    +                WindowAreaComponentApiV2Implementation::class.java, 2
    +            )
    +        )
    +
             assertFalse(
                 WindowAreaComponentValidator.isWindowAreaComponentValid(
    -                IncompleteWindowAreaComponentApiV2Implementation::class.java, 3))
    +                IncompleteWindowAreaComponentApiV2Implementation::class.java, 3
    +            )
    +        )
         }
     
         /**
    @@ -85,7 +99,9 @@
         }
     
         /**
    -     * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid
    +     * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid and expected to
    +     * be found on that vendorApiLevel. Verifies that true is returned if the vendorApiLevel is a
    +     * version before [ExtensionWindowAreaStatus] was introduced.
          */
         @Test
         fun isExtensionWindowAreaStatusValid_trueIfValid() {
    @@ -98,15 +114,13 @@
         }
     
         /**
    -     * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete
    +     * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete and
    +     * expected to be on the device.
          */
         @Test
         fun isExtensionWindowAreaStatusValid_falseIfIncomplete() {
             assertFalse(
                 WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
    -                IncompleteExtensionWindowAreaStatus::class.java, 2))
    -        assertFalse(
    -            WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
                     IncompleteExtensionWindowAreaStatus::class.java, 3))
         }
     
    
    diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
    index 05e23cc..f08a8e6 100644
    --- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
    
    @@ -17,20 +17,26 @@
     package androidx.window.embedding
     
     import android.app.Activity
    +import android.graphics.Color
     import android.os.Binder
     import android.os.IBinder
     import androidx.window.WindowSdkExtensions
     import androidx.window.WindowTestUtils
     import androidx.window.core.PredicateAdapter
    -import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_ACTIVITY_STACK_TOKEN
    -import androidx.window.embedding.EmbeddingAdapter.Companion.INVALID_SPLIT_INFO_TOKEN
    +import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
    +import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
    +import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
     import androidx.window.embedding.SplitAttributes.SplitType
     import androidx.window.embedding.SplitAttributes.SplitType.Companion.SPLIT_TYPE_HINGE
     import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
    +import androidx.window.extensions.embedding.ActivityStack.Token as OEMActivityStackToken
    +import androidx.window.extensions.embedding.AnimationBackground as OEMEmbeddingAnimationBackground
    +import androidx.window.extensions.embedding.DividerAttributes as OEMDividerAttributes
     import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
     import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
     import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
     import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
    +import androidx.window.extensions.embedding.SplitInfo.Token as OEMSplitInfoToken
     import org.junit.Assert.assertEquals
     import org.junit.Before
     import org.junit.Test
    @@ -39,6 +45,7 @@
     
     /** Tests for [EmbeddingAdapter] */
     class EmbeddingAdapterTest {
    +
         private lateinit var adapter: EmbeddingAdapter
     
         private val extensionVersion = WindowSdkExtensions.getInstance().extensionVersion
    @@ -51,47 +58,147 @@
         }
     
         @Test
    -    fun testTranslateSplitInfoWithDefaultAttrs() {
    +    fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel2() {
             WindowTestUtils.assumeAtLeastVendorApiLevel(2)
             WindowTestUtils.assumeBeforeVendorApiLevel(3)
     
    -        val oemSplitInfo = createTestOEMSplitInfo(
    -            createTestOEMActivityStack(ArrayList(), true),
    -            createTestOEMActivityStack(ArrayList(), true),
    -            OEMSplitAttributes.Builder().build(),
    -        )
    +        val oemSplitInfo = createTestOEMSplitInfo(OEMSplitAttributes.Builder().build())
             val expectedSplitInfo = SplitInfo(
    -            ActivityStack(ArrayList(), isEmpty = true),
    -            ActivityStack(ArrayList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     .build(),
    -            INVALID_SPLIT_INFO_TOKEN,
    +        )
    +        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
    +    }
    +
    +    @Suppress("DEPRECATION")
    +    @Test
    +    fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel3() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
    +        WindowTestUtils.assumeBeforeVendorApiLevel(5)
    +
    +        val oemSplitInfo = createTestOEMSplitInfo(
    +            OEMSplitAttributes.Builder().build(),
    +            testBinder = Binder(),
    +        )
    +        val expectedSplitInfo = SplitInfo(
    +            ActivityStack(emptyList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
    +            SplitAttributes.Builder()
    +                .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
    +                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    +                .build(),
    +            oemSplitInfo.token,
             )
             assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
         }
     
         @Test
    -    fun testTranslateSplitInfoWithExpandingContainers() {
    +    fun testTranslateSplitInfoWithDefaultAttrsWithApiLevel5() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
    +
    +        val oemSplitInfo = createTestOEMSplitInfo(
    +            createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
    +            createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
    +            OEMSplitAttributes.Builder().build(),
    +            testToken = OEMSplitInfoToken.createFromBinder(Binder()),
    +        )
    +        val expectedSplitInfo = SplitInfo(
    +            ActivityStack(
    +                ArrayList(),
    +                isEmpty = true,
    +                oemSplitInfo.primaryActivityStack.activityStackToken,
    +            ),
    +            ActivityStack(
    +                ArrayList(),
    +                isEmpty = true,
    +                oemSplitInfo.secondaryActivityStack.activityStackToken,
    +            ),
    +            SplitAttributes.Builder()
    +                .setSplitType(SplitType.SPLIT_TYPE_EQUAL)
    +                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    +                .build(),
    +            oemSplitInfo.splitInfoToken,
    +        )
    +        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
    +    }
    +
    +    @Test
    +    fun testTranslateSplitInfoWithExpandingContainersWithApiLevel2() {
             WindowTestUtils.assumeAtLeastVendorApiLevel(2)
             WindowTestUtils.assumeBeforeVendorApiLevel(3)
     
             val oemSplitInfo = createTestOEMSplitInfo(
    -            createTestOEMActivityStack(ArrayList(), true),
    -            createTestOEMActivityStack(ArrayList(), true),
                 OEMSplitAttributes.Builder()
                     .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
    -                .build(),
    +                .build()
             )
             val expectedSplitInfo = SplitInfo(
    -            ActivityStack(ArrayList(), isEmpty = true),
    -            ActivityStack(ArrayList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     .build(),
    -            INVALID_SPLIT_INFO_TOKEN,
    +        )
    +        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
    +    }
    +
    +    @Suppress("DEPRECATION")
    +    @Test
    +    fun testTranslateSplitInfoWithExpandingContainersWithApiLevel3() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(3)
    +        WindowTestUtils.assumeBeforeVendorApiLevel(5)
    +
    +        val oemSplitInfo = createTestOEMSplitInfo(
    +            OEMSplitAttributes.Builder()
    +                .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
    +                .build(),
    +            testBinder = Binder(),
    +        )
    +        val expectedSplitInfo = SplitInfo(
    +            ActivityStack(ArrayList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
    +            SplitAttributes.Builder()
    +                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
    +                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    +                .build(),
    +            oemSplitInfo.token,
    +        )
    +        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
    +    }
    +
    +    @Test
    +    fun testTranslateSplitInfoWithExpandingContainersWithApiLevel5() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
    +
    +        val oemSplitInfo = createTestOEMSplitInfo(
    +            createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
    +            createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
    +            OEMSplitAttributes.Builder()
    +                .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
    +                .build(),
    +            testToken = OEMSplitInfoToken.createFromBinder(Binder()),
    +        )
    +        val expectedSplitInfo = SplitInfo(
    +            ActivityStack(
    +                ArrayList(),
    +                isEmpty = true,
    +                oemSplitInfo.primaryActivityStack.activityStackToken
    +            ),
    +            ActivityStack(
    +                ArrayList(),
    +                isEmpty = true,
    +                oemSplitInfo.secondaryActivityStack.activityStackToken
    +            ),
    +            SplitAttributes.Builder()
    +                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
    +                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
    +                .build(),
    +            oemSplitInfo.splitInfoToken,
             )
             assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
         }
    @@ -110,14 +217,13 @@
             }
     
             val expectedSplitInfo = SplitInfo(
    -            ActivityStack(ArrayList(), isEmpty = true),
    -            ActivityStack(ArrayList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SplitType.ratio(expectedSplitRatio))
                     // OEMSplitInfo with Vendor API level 1 doesn't provide layoutDirection.
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                     .build(),
    -            INVALID_SPLIT_INFO_TOKEN,
             )
             assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
         }
    @@ -128,40 +234,34 @@
             WindowTestUtils.assumeBeforeVendorApiLevel(3)
     
             val oemSplitInfo = createTestOEMSplitInfo(
    -            createTestOEMActivityStack(ArrayList(), true),
    -            createTestOEMActivityStack(ArrayList(), true),
                 OEMSplitAttributes.Builder()
                     .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
                     .setLayoutDirection(TOP_TO_BOTTOM)
                     .build(),
             )
             val expectedSplitInfo = SplitInfo(
    -            ActivityStack(ArrayList(), isEmpty = true),
    -            ActivityStack(ArrayList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
    +            ActivityStack(emptyList(), isEmpty = true),
                 SplitAttributes.Builder()
                     .setSplitType(SPLIT_TYPE_HINGE)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
                     .build(),
    -            INVALID_SPLIT_INFO_TOKEN,
             )
             assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
         }
     
    +    @Suppress("DEPRECATION")
         @Test
         fun testTranslateSplitInfoWithApiLevel3() {
             WindowTestUtils.assumeAtLeastVendorApiLevel(3)
             WindowTestUtils.assumeBeforeVendorApiLevel(5)
     
    -        val testStackToken = Binder()
    -        val testSplitInfoToken = Binder()
             val oemSplitInfo = createTestOEMSplitInfo(
    -            createTestOEMActivityStack(ArrayList(), true, testStackToken),
    -            createTestOEMActivityStack(ArrayList(), true, testStackToken),
                 OEMSplitAttributes.Builder()
                     .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
                     .setLayoutDirection(TOP_TO_BOTTOM)
                     .build(),
    -            testSplitInfoToken,
    +            testBinder = Binder()
             )
             val expectedSplitInfo = SplitInfo(
                 ActivityStack(ArrayList(), isEmpty = true),
    @@ -170,16 +270,185 @@
                     .setSplitType(SPLIT_TYPE_HINGE)
                     .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
                     .build(),
    -            testSplitInfoToken,
    +            oemSplitInfo.token,
             )
             assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
         }
     
    +    @Test
    +    fun testTranslateSplitInfoWithApiLevel5() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
    +
    +        val oemSplitInfo = createTestOEMSplitInfo(
    +            createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
    +            createTestOEMActivityStack(OEMActivityStackToken.createFromBinder(Binder())),
    +            OEMSplitAttributes.Builder()
    +                .setSplitType(OEMSplitAttributes.SplitType.HingeSplitType(RatioSplitType(0.5f)))
    +                .setLayoutDirection(TOP_TO_BOTTOM)
    +                .build(),
    +            testToken = OEMSplitInfoToken.createFromBinder(Binder())
    +        )
    +        val expectedSplitInfo = SplitInfo(
    +            ActivityStack(
    +                emptyList(),
    +                isEmpty = true,
    +                oemSplitInfo.primaryActivityStack.activityStackToken,
    +            ),
    +            ActivityStack(
    +                emptyList(),
    +                isEmpty = true,
    +                oemSplitInfo.secondaryActivityStack.activityStackToken,
    +            ),
    +            SplitAttributes.Builder()
    +                .setSplitType(SPLIT_TYPE_HINGE)
    +                .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
    +                .build(),
    +            token = oemSplitInfo.splitInfoToken,
    +        )
    +        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
    +    }
    +
    +    @Test
    +    fun testTranslateAnimationBackgroundWithApiLevel5() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
    +
    +        val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
    +        val splitAttributesWithColorBackground = SplitAttributes.Builder()
    +            .setAnimationBackground(colorBackground)
    +            .build()
    +        val splitAttributesWithDefaultBackground = SplitAttributes.Builder()
    +            .setAnimationBackground(EmbeddingAnimationBackground.DEFAULT)
    +            .build()
    +
    +        val extensionsColorBackground =
    +            OEMEmbeddingAnimationBackground.createColorBackground(Color.BLUE)
    +        val extensionsSplitAttributesWithColorBackground = OEMSplitAttributes.Builder()
    +            .setAnimationBackground(extensionsColorBackground)
    +            .build()
    +        val extensionsSplitAttributesWithDefaultBackground = OEMSplitAttributes.Builder()
    +            .setAnimationBackground(OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT)
    +            .build()
    +
    +        // Translate from Window to Extensions
    +        assertEquals(
    +            extensionsSplitAttributesWithColorBackground,
    +            adapter.translateSplitAttributes(splitAttributesWithColorBackground)
    +        )
    +        assertEquals(
    +            extensionsSplitAttributesWithDefaultBackground,
    +            adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
    +        )
    +
    +        // Translate from Extensions to Window
    +        assertEquals(
    +            splitAttributesWithColorBackground,
    +            adapter.translate(extensionsSplitAttributesWithColorBackground)
    +        )
    +        assertEquals(
    +            splitAttributesWithDefaultBackground,
    +            adapter.translate(extensionsSplitAttributesWithDefaultBackground)
    +        )
    +    }
    +
    +    @Test
    +    fun testTranslateAnimationBackgroundBeforeApiLevel5() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(2)
    +        WindowTestUtils.assumeBeforeVendorApiLevel(5)
    +
    +        val colorBackground = EmbeddingAnimationBackground.createColorBackground(Color.BLUE)
    +        val splitAttributesWithColorBackground = SplitAttributes.Builder()
    +            .setAnimationBackground(colorBackground)
    +            .build()
    +        val splitAttributesWithDefaultBackground = SplitAttributes.Builder()
    +            .setAnimationBackground(EmbeddingAnimationBackground.DEFAULT)
    +            .build()
    +
    +        // No difference after translate before API level 5
    +        assertEquals(
    +            adapter.translateSplitAttributes(splitAttributesWithColorBackground),
    +            adapter.translateSplitAttributes(splitAttributesWithDefaultBackground)
    +        )
    +    }
    +
    +    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
    +    @Test
    +    fun testTranslateEmbeddingConfigurationToWindowAttributes() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(5)
    +
    +        val dimAreaBehavior = EmbeddingConfiguration.DimAreaBehavior.ON_TASK
    +        adapter.embeddingConfiguration = EmbeddingConfiguration(dimAreaBehavior)
    +        val oemSplitAttributes = adapter.translateSplitAttributes(SplitAttributes.Builder().build())
    +
    +        assertEquals(dimAreaBehavior.value, oemSplitAttributes.windowAttributes.dimAreaBehavior)
    +    }
    +
    +    @Test
    +    fun testTranslateDividerAttributes_draggable() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
    +        val dividerAttributes = DraggableDividerAttributes.Builder()
    +            .setWidthDp(20)
    +            .setDragRange(SplitRatioDragRange(0.3f, 0.7f))
    +            .setColor(Color.GRAY)
    +            .build()
    +        val oemDividerAttributes =
    +            OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE)
    +                .setWidthDp(20)
    +                .setPrimaryMinRatio(0.3f)
    +                .setPrimaryMaxRatio(0.7f)
    +                .setDividerColor(Color.GRAY)
    +                .build()
    +
    +        assertEquals(oemDividerAttributes, adapter.translateDividerAttributes(dividerAttributes))
    +        assertEquals(dividerAttributes, adapter.translateDividerAttributes(oemDividerAttributes))
    +    }
    +
    +    @Test
    +    fun testTranslateDividerAttributes_fixed() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
    +        val dividerAttributes = FixedDividerAttributes.Builder()
    +            .setWidthDp(20)
    +            .setColor(Color.GRAY)
    +            .build()
    +        val oemDividerAttributes =
    +            OEMDividerAttributes.Builder(OEMDividerAttributes.DIVIDER_TYPE_FIXED)
    +                .setWidthDp(20)
    +                .setDividerColor(Color.GRAY)
    +                .build()
    +
    +        assertEquals(oemDividerAttributes, adapter.translateDividerAttributes(dividerAttributes))
    +        assertEquals(dividerAttributes, adapter.translateDividerAttributes(oemDividerAttributes))
    +    }
    +
    +    @Test
    +    fun testTranslateDividerAttributes_noDivider() {
    +        WindowTestUtils.assumeAtLeastVendorApiLevel(6)
    +        val dividerAttributes = DividerAttributes.NO_DIVIDER
    +        val oemDividerAttributes = null
    +
    +        assertEquals(oemDividerAttributes, adapter.translateDividerAttributes(dividerAttributes))
    +        assertEquals(dividerAttributes, adapter.translateDividerAttributes(oemDividerAttributes))
    +    }
    +
    +    private fun createTestOEMSplitInfo(
    +        testSplitAttributes: OEMSplitAttributes,
    +        testBinder: IBinder? = null,
    +        testToken: OEMSplitInfo.Token? = null,
    +    ): OEMSplitInfo =
    +        createTestOEMSplitInfo(
    +            createTestOEMActivityStack(),
    +            createTestOEMActivityStack(),
    +            testSplitAttributes,
    +            testBinder,
    +            testToken,
    +        )
    +
    +    @Suppress("Deprecation") // Verify the behavior of version 3 and 4.
         private fun createTestOEMSplitInfo(
             testPrimaryActivityStack: OEMActivityStack,
             testSecondaryActivityStack: OEMActivityStack,
             testSplitAttributes: OEMSplitAttributes,
    -        testToken: IBinder = INVALID_SPLIT_INFO_TOKEN,
    +        testBinder: IBinder? = null,
    +        testToken: OEMSplitInfoToken? = null,
         ): OEMSplitInfo {
             return mock().apply {
                 whenever(primaryActivityStack).thenReturn(testPrimaryActivityStack)
    @@ -187,22 +456,31 @@
                 if (extensionVersion >= 2) {
                     whenever(splitAttributes).thenReturn(testSplitAttributes)
                 }
    -            if (extensionVersion >= 3) {
    -                whenever(token).thenReturn(testToken)
    +            when (extensionVersion) {
    +                in 3..4 -> whenever(token).thenReturn(testBinder)
    +                in 5..Int.MAX_VALUE -> whenever(splitInfoToken).thenReturn(testToken)
                 }
             }
         }
     
         private fun createTestOEMActivityStack(
    +        testToken: OEMActivityStackToken? = null
    +    ): OEMActivityStack = createTestOEMActivityStack(
    +        emptyList(),
    +        testIsEmpty = true,
    +        testToken,
    +    )
    +
    +    private fun createTestOEMActivityStack(
             testActivities: List,
             testIsEmpty: Boolean,
    -        testToken: IBinder = INVALID_ACTIVITY_STACK_TOKEN,
    +        testToken: OEMActivityStackToken? = null,
         ): OEMActivityStack {
             return mock().apply {
                 whenever(activities).thenReturn(testActivities)
                 whenever(isEmpty).thenReturn(testIsEmpty)
    -            if (extensionVersion in 3..4) {
    -                whenever(token).thenReturn(testToken)
    +            if (extensionVersion >= 5) {
    +                whenever(activityStackToken).thenReturn(testToken)
                 }
             }
         }
    
    diff --git a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
    index 2b62a06..0f97261 100644
    --- a/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
    +++ b/window/window/src/androidTest/java/androidx/window/embedding/RuleParserTests.kt
    
    @@ -19,6 +19,7 @@
     import android.content.ComponentName
     import android.content.Context
     import android.content.Intent
    +import android.graphics.Color
     import android.graphics.Rect
     import android.os.Build
     import androidx.annotation.RequiresApi
    @@ -77,6 +78,7 @@
             val expectedSplitLayout = SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
                 .setLayoutDirection(LOCALE)
    +            .setAnimationBackground(EmbeddingAnimationBackground.DEFAULT)
                 .build()
             assertNull(rule.tag)
             assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
    @@ -128,6 +130,7 @@
             val expectedSplitLayout = SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
                 .setLayoutDirection(TOP_TO_BOTTOM)
    +            .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(Color.BLUE))
                 .build()
             assertEquals(TEST_TAG, rule.tag)
             assertEquals(NEVER, rule.finishPrimaryWithSecondary)
    @@ -151,6 +154,7 @@
             val expectedSplitLayout = SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
                 .setLayoutDirection(LOCALE)
    +            .setAnimationBackground(EmbeddingAnimationBackground.DEFAULT)
                 .build()
             assertNull(rule.tag)
             assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
    @@ -205,6 +209,8 @@
             val expectedSplitLayout = SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
                 .setLayoutDirection(BOTTOM_TO_TOP)
    +            .setAnimationBackground(EmbeddingAnimationBackground.createColorBackground(
    +                application.resources.getColor(R.color.testColor, null)))
                 .build()
             assertEquals(TEST_TAG, rule.tag)
             assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
    
    diff --git a/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
    index 7f0659e..de08762 100644
    --- a/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/embedding/SafeActivityEmbeddingComponentProviderTest.kt
    
    @@ -17,8 +17,8 @@
     package androidx.window.embedding
     
     import android.util.Log
    +import androidx.window.WindowSdkExtensions
     import androidx.window.core.ConsumerAdapter
    -import androidx.window.core.ExtensionsUtil
     import androidx.window.extensions.WindowExtensions
     import androidx.window.extensions.WindowExtensionsProvider
     import org.junit.Assert.assertNotNull
    @@ -67,9 +67,12 @@
                     // TODO(b/267708462) : more reliable test for testing actual method matching
                     assertNotNull(safeComponent)
                     assertTrue(safeProvider.isActivityEmbeddingComponentAccessible())
    -                when (ExtensionsUtil.safeVendorApiLevel) {
    +                when (WindowSdkExtensions.getInstance().extensionVersion) {
                         1 -> assertTrue(safeProvider.hasValidVendorApiLevel1())
    -                    else -> assertTrue(safeProvider.hasValidVendorApiLevel2())
    +                    2 -> assertTrue(safeProvider.hasValidVendorApiLevel2())
    +                    in 3..4 -> assertTrue(safeProvider.hasValidVendorApiLevel3())
    +                    5 -> assertTrue(safeProvider.hasValidVendorApiLevel5())
    +                    6 -> assertTrue(safeProvider.hasValidVendorApiLevel6())
                     }
                 }
             } catch (e: UnsupportedOperationException) {
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
    index 784a5c5..1c166e87 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/SafeWindowLayoutComponentProviderTest.kt
    
    @@ -52,9 +52,14 @@
                     // TODO(b/267708462): more reliable test for testing actual method matching
                     assertNotNull(safeComponent)
                     assertTrue(safeProvider.isWindowLayoutComponentAccessible())
    -                when (ExtensionsUtil.safeVendorApiLevel) {
    -                    1 -> assertTrue(safeProvider.hasValidVendorApiLevel1())
    -                    else -> assertTrue(safeProvider.hasValidVendorApiLevel2())
    +                when {
    +                    ExtensionsUtil.safeVendorApiLevel == 1 -> {
    +                        assertTrue(safeProvider.hasValidVendorApiLevel1())
    +                    }
    +                    ExtensionsUtil.safeVendorApiLevel < 6 -> {
    +                        assertTrue(safeProvider.hasValidVendorApiLevel2())
    +                    }
    +                    else -> assertTrue(safeProvider.hasValidVendorApiLevel6())
                     }
                 }
             } catch (e: UnsupportedOperationException) {
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
    index 84da641..0026ec9 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
    
    @@ -22,10 +22,13 @@
     import androidx.test.ext.junit.rules.ActivityScenarioRule
     import androidx.window.TestActivity
     import androidx.window.TestConsumer
    +import androidx.window.WindowSdkExtensions
     import androidx.window.WindowTestUtils
     import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
    +import androidx.window.WindowTestUtils.Companion.assumeBeforeVendorApiLevel
     import androidx.window.layout.adapter.WindowBackend
     import java.util.concurrent.Executor
    +import kotlin.test.assertEquals
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.ExperimentalCoroutinesApi
     import kotlinx.coroutines.Job
    @@ -34,6 +37,7 @@
     import kotlinx.coroutines.test.UnconfinedTestDispatcher
     import kotlinx.coroutines.test.runTest
     import kotlinx.coroutines.test.setMain
    +import org.junit.Assert.assertThrows
     import org.junit.Rule
     import org.junit.Test
     
    @@ -45,6 +49,7 @@
             ActivityScenarioRule(TestActivity::class.java)
     
         private val testScope = TestScope(UnconfinedTestDispatcher())
    +    private val windowSdkExtensions = WindowSdkExtensions.getInstance()
     
         init {
             Dispatchers.setMain(UnconfinedTestDispatcher())
    @@ -53,11 +58,12 @@
         @Test
         public fun testWindowLayoutFeatures(): Unit = testScope.runTest {
             activityScenario.scenario.onActivity { testActivity ->
    -            val windowMetricsCalculator = WindowMetricsCalculatorCompat
    +            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
                 val fakeBackend = FakeWindowBackend()
                 val repo = WindowInfoTrackerImpl(
                     windowMetricsCalculator,
    -                fakeBackend
    +                fakeBackend,
    +                windowSdkExtensions
                 )
                 val collector = TestConsumer()
                 testScope.launch(Job()) {
    @@ -75,7 +81,11 @@
             }
             assumeAtLeastVendorApiLevel(2)
             val fakeBackend = FakeWindowBackend()
    -        val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, fakeBackend)
    +        val repo = WindowInfoTrackerImpl(
    +            WindowMetricsCalculatorCompat(),
    +            fakeBackend,
    +            windowSdkExtensions
    +        )
             val collector = TestConsumer()
     
             val windowContext =
    @@ -90,11 +100,12 @@
         @Test
         public fun testWindowLayoutFeatures_multicasting(): Unit = testScope.runTest {
             activityScenario.scenario.onActivity { testActivity ->
    -            val windowMetricsCalculator = WindowMetricsCalculatorCompat
    +            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
                 val fakeBackend = FakeWindowBackend()
                 val repo = WindowInfoTrackerImpl(
                     windowMetricsCalculator,
    -                fakeBackend
    +                fakeBackend,
    +                windowSdkExtensions
                 )
                 val collector = TestConsumer()
                 val job = Job()
    @@ -113,16 +124,52 @@
         }
     
         @Test
    +    fun testSupportedWindowPostures_throwsBeforeApi6() {
    +        assumeBeforeVendorApiLevel(6)
    +        activityScenario.scenario.onActivity { _ ->
    +            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
    +            val fakeBackend = FakeWindowBackend()
    +            val repo = WindowInfoTrackerImpl(
    +                windowMetricsCalculator,
    +                fakeBackend,
    +                windowSdkExtensions
    +            )
    +            assertThrows(UnsupportedOperationException::class.java) {
    +                repo.supportedPostures
    +            }
    +        }
    +    }
    +
    +    @Test
    +    fun testSupportedWindowPostures_reportsFeatures() {
    +        assumeAtLeastVendorApiLevel(6)
    +        activityScenario.scenario.onActivity { _ ->
    +            val windowMetricsCalculator = WindowMetricsCalculatorCompat()
    +            val expected = listOf(SupportedPosture.TABLETOP)
    +            val fakeBackend = FakeWindowBackend(supportedPostures = expected)
    +            val repo = WindowInfoTrackerImpl(
    +                windowMetricsCalculator,
    +                fakeBackend,
    +                windowSdkExtensions
    +            )
    +            val actual = repo.supportedPostures
    +
    +            assertEquals(expected, actual)
    +        }
    +    }
    +
    +    @Test
         public fun testWindowLayoutFeatures_multicastingWithContext(): Unit = testScope.runTest {
             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
                 return@runTest
             }
             assumeAtLeastVendorApiLevel(2)
    -        val windowMetricsCalculator = WindowMetricsCalculatorCompat
    +        val windowMetricsCalculator = WindowMetricsCalculatorCompat()
             val fakeBackend = FakeWindowBackend()
             val repo = WindowInfoTrackerImpl(
                 windowMetricsCalculator,
    -            fakeBackend
    +            fakeBackend,
    +            windowSdkExtensions
             )
             val collector = TestConsumer()
             val job = Job()
    @@ -143,7 +190,9 @@
             )
         }
     
    -    private class FakeWindowBackend : WindowBackend {
    +    private class FakeWindowBackend(
    +        override val supportedPostures: List = emptyList()
    +    ) : WindowBackend {
     
             private class CallbackHolder(
                 val executor: Executor,
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
    index 117fd2e..270fdb9 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsCalculatorCompatTest.kt
    
    @@ -16,19 +16,24 @@
     package androidx.window.layout
     
     import android.annotation.SuppressLint
    -import android.app.Activity
     import android.content.ContextWrapper
     import android.os.Build
     import android.view.Display
     import android.view.WindowManager
    +import androidx.annotation.RequiresApi
     import androidx.core.view.WindowInsetsCompat
    -import androidx.lifecycle.Lifecycle
     import androidx.test.core.app.ActivityScenario.ActivityAction
     import androidx.test.ext.junit.rules.ActivityScenarioRule
     import androidx.test.ext.junit.runners.AndroidJUnit4
     import androidx.test.filters.LargeTest
     import androidx.window.TestActivity
    +import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeR
    +import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
    +import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove
    +import androidx.window.WindowTestUtils.Companion.isInMultiWindowMode
    +import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle
     import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
     import org.junit.Assert.assertEquals
     import org.junit.Assert.assertNotEquals
     import org.junit.Assume
    @@ -124,8 +129,9 @@
         @Test
         fun testGetCurrentWindowBounds_postR() {
             assumePlatformROrAbove()
    -        runActionsAcrossActivityLifecycle({ }) { activity: TestActivity ->
    -            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val bounds = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity).bounds
                 val windowMetricsBounds = activity.windowManager.currentWindowMetrics.bounds
                 assertEquals(windowMetricsBounds, bounds)
             }
    @@ -199,8 +205,9 @@
         @Test
         fun testGetMaximumWindowBounds_postR() {
             assumePlatformROrAbove()
    -        runActionsAcrossActivityLifecycle({ }) { activity: TestActivity ->
    -            val bounds = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity).bounds
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val bounds = WindowMetricsCalculatorCompat()
    +                .computeMaximumWindowMetrics(activity).bounds
                 val windowMetricsBounds = activity.windowManager.maximumWindowMetrics.bounds
                 assertEquals(windowMetricsBounds, bounds)
             }
    @@ -211,8 +218,9 @@
         @OptIn(ExperimentalWindowApi::class)
         fun testGetWindowInsetsCompat_currentWindowMetrics_postR() {
             assumePlatformROrAbove()
    -        runActionsAcrossActivityLifecycle({ }) { activity: TestActivity ->
    -            val windowMetrics = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity)
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val windowMetrics = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity)
                 val windowInsets = windowMetrics.getWindowInsets()
                 val platformInsets = activity.windowManager.currentWindowMetrics.windowInsets
                 val platformWindowInsets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
    @@ -225,8 +233,9 @@
         @OptIn(ExperimentalWindowApi::class)
         fun testGetWindowInsetsCompat_maximumWindowMetrics_postR() {
             assumePlatformROrAbove()
    -        runActionsAcrossActivityLifecycle({ }) { activity: TestActivity ->
    -            val windowMetrics = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity)
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val windowMetrics = WindowMetricsCalculatorCompat()
    +                .computeMaximumWindowMetrics(activity)
                 val windowInsets = windowMetrics.getWindowInsets()
                 val platformInsets = activity.windowManager.maximumWindowMetrics.windowInsets
                 val platformWindowInsets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
    @@ -234,12 +243,39 @@
             }
         }
     
    +    @Test
    +    fun testDensityMatchesDisplayMetricsDensity() {
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val calculator = WindowMetricsCalculatorCompat()
    +            val windowMetrics = calculator.computeCurrentWindowMetrics(activity)
    +            val maxWindowMetrics = calculator.computeMaximumWindowMetrics(
    +                activity)
    +            assertEquals(activity.resources.displayMetrics.density, windowMetrics.density)
    +            assertEquals(windowMetrics.density, maxWindowMetrics.density)
    +        }
    +    }
    +
    +    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +    @Test
    +    fun testConvertedWindowMetricsMatchesPlatformWindowMetrics() {
    +        assumePlatformUOrAbove()
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val calculator = WindowMetricsCalculatorCompat()
    +            val windowMetrics = calculator.computeCurrentWindowMetrics(activity)
    +            val wm = activity.getSystemService(WindowManager::class.java)
    +            val androidWindowMetrics = wm.currentWindowMetrics
    +            assertEquals(androidWindowMetrics.bounds, windowMetrics.bounds)
    +            assertEquals(androidWindowMetrics.density, windowMetrics.density)
    +        }
    +    }
    +
         private fun testGetCurrentWindowBoundsMatchesRealDisplaySize(
             initialAction: ActivityAction
         ) {
             val assertWindowBoundsMatchesDisplayAction: ActivityAction =
                 AssertCurrentWindowBoundsEqualsRealDisplaySizeAction()
    -        runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction)
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, initialAction,
    +            assertWindowBoundsMatchesDisplayAction)
         }
     
         private fun testGetMaximumWindowBoundsMatchesRealDisplaySize(
    @@ -247,28 +283,8 @@
         ) {
             val assertWindowBoundsMatchesDisplayAction: ActivityAction =
                 AssertMaximumWindowBoundsEqualsRealDisplaySizeAction()
    -        runActionsAcrossActivityLifecycle(initialAction, assertWindowBoundsMatchesDisplayAction)
    -    }
    -
    -    /**
    -     * Creates and launches an activity performing the supplied actions at various points in the
    -     * activity lifecycle.
    -     *
    -     * @param initialAction the action that will run once before the activity is created.
    -     * @param verifyAction the action to run once after each change in activity lifecycle state.
    -     */
    -    private fun runActionsAcrossActivityLifecycle(
    -        initialAction: ActivityAction,
    -        verifyAction: ActivityAction
    -    ) {
    -        val scenario = activityScenarioRule.scenario
    -        scenario.onActivity(initialAction)
    -        scenario.moveToState(Lifecycle.State.CREATED)
    -        scenario.onActivity(verifyAction)
    -        scenario.moveToState(Lifecycle.State.STARTED)
    -        scenario.onActivity(verifyAction)
    -        scenario.moveToState(Lifecycle.State.RESUMED)
    -        scenario.onActivity(verifyAction)
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, initialAction,
    +            assertWindowBoundsMatchesDisplayAction)
         }
     
         private fun assumeNotMultiWindow() {
    @@ -299,8 +315,9 @@
                     @Suppress("DEPRECATION")
                     activity.windowManager.defaultDisplay
                 }
    -            val realDisplaySize = WindowMetricsCalculatorCompat.getRealSizeForDisplay(display)
    -            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
    +            val calculator = WindowMetricsCalculatorCompat()
    +            val realDisplaySize = getRealSizeForDisplay(display)
    +            val bounds = calculator.computeCurrentWindowMetrics(activity).bounds
                 assertNotEquals("Device can not have zero width", 0, realDisplaySize.x.toLong())
                 assertNotEquals("Device can not have zero height", 0, realDisplaySize.y.toLong())
                 assertEquals(
    @@ -323,8 +340,9 @@
                     @Suppress("DEPRECATION")
                     activity.windowManager.defaultDisplay
                 }
    -            val realDisplaySize = WindowMetricsCalculatorCompat.getRealSizeForDisplay(display)
    -            val bounds = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(activity).bounds
    +            val calculator = WindowMetricsCalculatorCompat()
    +            val realDisplaySize = getRealSizeForDisplay(display)
    +            val bounds = calculator.computeMaximumWindowMetrics(activity).bounds
                 assertEquals(
                     "Window bounds width does not match real display width",
                     realDisplaySize.x.toLong(), bounds.width().toLong()
    @@ -335,20 +353,4 @@
                 )
             }
         }
    -
    -    private companion object {
    -        private fun isInMultiWindowMode(activity: Activity): Boolean {
    -            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    -                activity.isInMultiWindowMode
    -            } else false
    -        }
    -
    -        private fun assumePlatformBeforeR() {
    -            Assume.assumeTrue(Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
    -        }
    -
    -        private fun assumePlatformROrAbove() {
    -            Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
    -        }
    -    }
     }
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
    index 756fc10..ed6b57e 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/WindowMetricsTest.kt
    
    @@ -34,81 +34,89 @@
     @RunWith(AndroidJUnit4::class)
     public class WindowMetricsTest {
         @Test
    -    public fun testGetBounds() {
    +    fun testGetBounds() {
             val bounds = Rect(1, 2, 3, 4)
    -        val windowMetrics = WindowMetrics(bounds)
    +        val windowMetrics = WindowMetrics(bounds, density = 1f)
             assertEquals(bounds, windowMetrics.bounds)
         }
     
         @Test
    -    public fun testEquals_sameBounds() {
    +    fun testEquals_sameBounds() {
             val bounds = Rect(1, 2, 3, 4)
    -        val windowMetrics0 = WindowMetrics(bounds)
    -        val windowMetrics1 = WindowMetrics(bounds)
    +        val windowMetrics0 = WindowMetrics(bounds, density = 1f)
    +        val windowMetrics1 = WindowMetrics(bounds, density = 1f)
             assertEquals(windowMetrics0, windowMetrics1)
         }
     
         @Test
    -    public fun testEquals_differentBounds() {
    +    fun testEquals_differentBounds() {
             val bounds0 = Rect(1, 2, 3, 4)
    -        val windowMetrics0 = WindowMetrics(bounds0)
    +        val windowMetrics0 = WindowMetrics(bounds0, density = 1f)
             val bounds1 = Rect(6, 7, 8, 9)
    -        val windowMetrics1 = WindowMetrics(bounds1)
    +        val windowMetrics1 = WindowMetrics(bounds1, density = 1f)
             assertNotEquals(windowMetrics0, windowMetrics1)
         }
     
         @Test
    -    public fun testHashCode_matchesIfEqual() {
    +    fun testEquals_differentDensities() {
             val bounds = Rect(1, 2, 3, 4)
    -        val windowMetrics0 = WindowMetrics(bounds)
    -        val windowMetrics1 = WindowMetrics(bounds)
    +        val windowMetrics0 = WindowMetrics(bounds, density = 0f)
    +        val windowMetrics1 = WindowMetrics(bounds, density = 1f)
    +        assertNotEquals(windowMetrics0, windowMetrics1)
    +    }
    +
    +    @Test
    +    fun testHashCode_matchesIfEqual() {
    +        val bounds = Rect(1, 2, 3, 4)
    +        val windowMetrics0 = WindowMetrics(bounds, density = 1f)
    +        val windowMetrics1 = WindowMetrics(bounds, density = 1f)
             assertEquals(windowMetrics0.hashCode().toLong(), windowMetrics1.hashCode().toLong())
         }
     
         @RequiresApi(Build.VERSION_CODES.R)
         @Test
    -    public fun testSameWindowInsets_emptyInsets() {
    +    fun testSameWindowInsets_emptyInsets() {
             assumePlatformROrAbove()
             val bounds = Bounds(1, 2, 3, 4)
             val windowInsetsCompat = WindowInsetsCompat.Builder().build()
    -        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)
    -        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)
    +        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
    +        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
             assertEquals(windowMetricsA, windowMetricsB)
         }
     
         @RequiresApi(Build.VERSION_CODES.R)
         @Test
    -    public fun testSameWindowInsets_nonEmptyInsets() {
    +    fun testSameWindowInsets_nonEmptyInsets() {
             assumePlatformROrAbove()
             val bounds = Bounds(1, 2, 3, 4)
             val insets = Insets.of(6, 7, 8, 9)
             val builder = WindowInsetsCompat.Builder()
    -        for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {
    +        for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) {
                 builder.setInsets(type, insets)
             }
             val windowInsetsCompat = builder.build()
    -        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat)
    -        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat)
    +        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
    +        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompat, 1f /* density */)
             assertEquals(windowMetricsA, windowMetricsB)
         }
     
         @RequiresApi(Build.VERSION_CODES.R)
         @Test
    -    public fun testDiffWindowInsets() {
    +    fun testDiffWindowInsets() {
             assumePlatformROrAbove()
             val bounds = Bounds(1, 2, 3, 4)
             val insetsA = Insets.of(1, 2, 3, 4)
             val insetsB = Insets.of(6, 7, 8, 9)
             val builderA = WindowInsetsCompat.Builder()
             val builderB = WindowInsetsCompat.Builder()
    -        for (type in WindowMetricsCalculatorCompat.insetsTypeMasks) {
    +        for (type in WindowMetricsCalculatorCompat().insetsTypeMasks) {
                 builderA.setInsets(type, insetsA)
                 builderB.setInsets(type, insetsB)
             }
             val windowInsetsCompatA = builderA.build()
             val windowInsetsCompatB = builderB.build()
    -        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA)
    -        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB)
    +        val windowMetricsA = WindowMetrics(bounds, windowInsetsCompatA, 1f /* density */)
    +        val windowMetricsB = WindowMetrics(bounds, windowInsetsCompatB, 1f /* density */)
             assertNotEquals(windowMetricsA, windowMetricsB)
         }
     
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
    index b1715fd..02f06025 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendTest.kt
    
    @@ -33,17 +33,21 @@
     import androidx.window.core.ConsumerAdapter
     import androidx.window.core.ExtensionsUtil
     import androidx.window.extensions.core.util.function.Consumer as OEMConsumer
    +import androidx.window.extensions.layout.DisplayFoldFeature
     import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
     import androidx.window.extensions.layout.FoldingFeature.STATE_FLAT
     import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
    +import androidx.window.extensions.layout.SupportedWindowFeatures
     import androidx.window.extensions.layout.WindowLayoutComponent
     import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
    +import androidx.window.layout.SupportedPosture
     import androidx.window.layout.WindowLayoutInfo
     import androidx.window.layout.WindowMetricsCalculatorCompat
     import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter.translate
     import java.util.function.Consumer as JavaConsumer
     import org.junit.Assert.assertEquals
     import org.junit.Assert.assertFalse
    +import org.junit.Assert.assertThrows
     import org.junit.Assert.assertTrue
     import org.junit.Assume.assumeTrue
     import org.junit.Before
    @@ -670,9 +674,49 @@
             consumer.assertValues(expected)
         }
     
    +    @Test
    +    fun testSupportedFeatures_throwsBeforeApi6() {
    +        assumeBeforeVendorApiLevel(6)
    +
    +        val component = FakeWindowComponent()
    +        val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
    +
    +        assertThrows(UnsupportedOperationException::class.java) {
    +            backend.supportedPostures
    +        }
    +    }
    +
    +    @Test
    +    fun testSupportedFeatures_emptyListReturnsNoFeatures() {
    +        assumeAtLeastVendorApiLevel(6)
    +
    +        val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf()).build()
    +        val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
    +        val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
    +
    +        val actual = backend.supportedPostures
    +        assertEquals(emptyList(), actual)
    +    }
    +
    +    @Test
    +    fun testSupportedFeatures_halfOpenedReturnsTabletopSupport() {
    +        assumeAtLeastVendorApiLevel(6)
    +
    +        val foldFeature = DisplayFoldFeature.Builder(DisplayFoldFeature.TYPE_SCREEN_FOLD_IN)
    +            .addProperties(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
    +            .build()
    +        val supportedWindowFeatures = SupportedWindowFeatures.Builder(listOf(foldFeature)).build()
    +        val component = FakeWindowComponent(windowFeatures = supportedWindowFeatures)
    +        val backend = ExtensionWindowBackend.newInstance(component, consumerAdapter)
    +
    +        val actual = backend.supportedPostures
    +        assertEquals(listOf(SupportedPosture.TABLETOP), actual)
    +    }
    +
         internal companion object {
             private fun newTestOEMWindowLayoutInfo(activity: Activity): OEMWindowLayoutInfo {
    -            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
    +            val bounds = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity).bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val feature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_FLAT)
                 val displayFeatures = listOf(feature)
    @@ -686,7 +730,7 @@
              */
             @RequiresApi(Build.VERSION_CODES.R)
             private fun newTestOEMWindowLayoutInfo(@UiContext context: Context): OEMWindowLayoutInfo {
    -            val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(context).bounds
    +            val bounds = WindowMetricsCalculatorCompat().computeCurrentWindowMetrics(context).bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val feature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_FLAT)
                 val displayFeatures = listOf(feature)
    @@ -722,7 +766,9 @@
             }
         }
     
    -    private class FakeWindowComponent : WindowLayoutComponent {
    +    private class FakeWindowComponent(
    +        private val windowFeatures: SupportedWindowFeatures? = null
    +    ) : WindowLayoutComponent {
     
             val consumers = mutableListOf>()
             val oemConsumers = mutableListOf>()
    @@ -751,6 +797,12 @@
                 oemConsumers.remove(consumer)
             }
     
    +        override fun getSupportedWindowFeatures(): SupportedWindowFeatures {
    +            return windowFeatures ?: throw UnsupportedOperationException(
    +                "Window features are not set. Either the vendor API level is too low or value " +
    +                    "was not set")
    +        }
    +
             @SuppressLint("NewApi")
             fun emit(info: OEMWindowLayoutInfo) {
                 if (ExtensionsUtil.safeVendorApiLevel < 2) {
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
    index 02f89fe..0539572 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapterTest.kt
    
    @@ -32,7 +32,7 @@
     import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
     import androidx.window.layout.TestFoldingFeatureUtil.invalidNonZeroFoldBounds
     import androidx.window.layout.WindowLayoutInfo
    -import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
    +import androidx.window.layout.WindowMetricsCalculatorCompat
     import org.junit.Assert.assertEquals
     import org.junit.Assert.assertNull
     import org.junit.Assert.assertTrue
    @@ -50,7 +50,8 @@
         @Test
         fun testTranslate_foldingFeature() {
             activityScenario.scenario.onActivity { activity ->
    -            val windowMetrics = computeCurrentWindowMetrics(activity)
    +            val windowMetrics = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity)
                 val bounds = windowMetrics.bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
    @@ -66,7 +67,8 @@
         @Test
         fun testTranslate_windowLayoutInfo() {
             activityScenario.scenario.onActivity { activity ->
    -            val bounds = computeCurrentWindowMetrics(activity).bounds
    +            val bounds = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity).bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
                 val oemInfo = OEMWindowLayoutInfo(listOf(oemFeature))
    @@ -84,7 +86,8 @@
         fun testTranslate_windowLayoutInfoFromContext() {
             assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
             activityScenario.scenario.onActivity { activity ->
    -            val bounds = computeCurrentWindowMetrics(activity).bounds
    +            val bounds = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity).bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, STATE_HALF_OPENED)
                 val oemInfo = OEMWindowLayoutInfo(listOf(oemFeature))
    @@ -101,7 +104,8 @@
         @Test
         fun testTranslate_foldingFeature_invalidType() {
             activityScenario.scenario.onActivity { activity ->
    -            val windowMetrics = computeCurrentWindowMetrics(activity)
    +            val windowMetrics = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity)
                 val bounds = windowMetrics.bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val oemFeature = OEMFoldingFeature(featureBounds, -1, STATE_HALF_OPENED)
    @@ -115,7 +119,8 @@
         @Test
         fun testTranslate_foldingFeature_invalidState() {
             activityScenario.scenario.onActivity { activity ->
    -            val windowMetrics = computeCurrentWindowMetrics(activity)
    +            val windowMetrics = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity)
                 val bounds = windowMetrics.bounds
                 val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
                 val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, -1)
    @@ -129,7 +134,8 @@
         @Test
         fun testTranslate_foldingFeature_invalidBounds() {
             activityScenario.scenario.onActivity { activity ->
    -            val windowMetrics = computeCurrentWindowMetrics(activity)
    +            val windowMetrics = WindowMetricsCalculatorCompat()
    +                .computeCurrentWindowMetrics(activity)
                 val windowBounds = windowMetrics.bounds
     
                 val source = invalidNonZeroFoldBounds(windowBounds)
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
    index 8a36b00..3c0cbdab 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarCompatDeviceTest.kt
    
    @@ -42,6 +42,7 @@
     import org.junit.Assert.assertNotNull
     import org.junit.Assume.assumeTrue
     import org.junit.Before
    +import org.junit.Ignore
     import org.junit.Test
     import org.junit.runner.RunWith
     import org.mockito.ArgumentMatcher
    @@ -87,6 +88,7 @@
             }
         }
     
    +    @Ignore("b/241174887")
         @Test
         fun testWindowLayoutCallbackOnConfigChange() {
             val testScope = TestScope(UnconfinedTestDispatcher())
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
    index 43daf25..bdf4c3c 100644
    --- a/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
    +++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendTest.kt
    
    @@ -200,6 +200,13 @@
             secondConsumer.assertValues(secondExpected)
         }
     
    +    @Test(expected = UnsupportedOperationException::class)
    +    fun testSupportedWindowFeatures_throws() {
    +        val interfaceCompat = SwitchOnUnregisterExtensionInterfaceCompat()
    +        val backend = SidecarWindowBackend(interfaceCompat)
    +        backend.supportedPostures
    +    }
    +
         internal companion object {
             private fun newTestWindowLayoutInfo(): WindowLayoutInfo {
                 val feature1: DisplayFeature = HardwareFoldingFeature(Bounds(0, 2, 3, 4), HINGE, FLAT)
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt b/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt
    new file mode 100644
    index 0000000..17e2120
    --- /dev/null
    +++ b/window/window/src/androidTest/java/androidx/window/layout/util/BoundsHelperTest.kt
    
    @@ -0,0 +1,66 @@
    +/*
    + * 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 androidx.window.layout.util
    +
    +import android.os.Build
    +import android.view.WindowManager
    +import androidx.annotation.RequiresApi
    +import androidx.test.ext.junit.rules.ActivityScenarioRule
    +import androidx.test.ext.junit.runners.AndroidJUnit4
    +import androidx.test.filters.LargeTest
    +import androidx.window.TestActivity
    +import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
    +import org.junit.Assert.assertEquals
    +import org.junit.Rule
    +import org.junit.Test
    +import org.junit.runner.RunWith
    +
    +/**
    + * Tests for [BoundsHelper].
    + */
    +@LargeTest
    +@RunWith(AndroidJUnit4::class)
    +class BoundsHelperTest {
    +
    +    @get:Rule
    +    var activityScenarioRule: ActivityScenarioRule =
    +        ActivityScenarioRule(TestActivity::class.java)
    +
    +    @RequiresApi(Build.VERSION_CODES.R)
    +    @Test
    +    fun testCurrentWindowBounds_postR() {
    +        assumePlatformROrAbove()
    +        activityScenarioRule.scenario.onActivity { activity ->
    +            val currentBounds = BoundsHelper.getInstance().currentWindowBounds(activity)
    +            val expectedBounds = activity.getSystemService(WindowManager::class.java)
    +                .currentWindowMetrics.bounds
    +            assertEquals(expectedBounds, currentBounds)
    +        }
    +    }
    +
    +    @RequiresApi(Build.VERSION_CODES.R)
    +    @Test
    +    fun testMaximumWindowBounds_postR() {
    +        assumePlatformROrAbove()
    +        activityScenarioRule.scenario.onActivity { activity ->
    +            val currentBounds = BoundsHelper.getInstance().maximumWindowBounds(activity)
    +            val expectedBounds = activity.getSystemService(WindowManager::class.java)
    +                .maximumWindowMetrics.bounds
    +            assertEquals(expectedBounds, currentBounds)
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt b/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt
    new file mode 100644
    index 0000000..d4ab5a8
    --- /dev/null
    +++ b/window/window/src/androidTest/java/androidx/window/layout/util/DensityCompatHelperTest.kt
    
    @@ -0,0 +1,107 @@
    +/*
    + * 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 androidx.window.layout.util
    +
    +import android.graphics.Rect
    +import android.os.Build
    +import android.util.DisplayMetrics
    +import android.view.WindowInsets
    +import android.view.WindowManager
    +import android.view.WindowMetrics as AndroidWindowMetrics
    +import androidx.annotation.RequiresApi
    +import androidx.test.ext.junit.rules.ActivityScenarioRule
    +import androidx.test.ext.junit.runners.AndroidJUnit4
    +import androidx.test.filters.LargeTest
    +import androidx.window.TestActivity
    +import androidx.window.WindowTestUtils.Companion.assumePlatformBeforeU
    +import androidx.window.WindowTestUtils.Companion.assumePlatformROrAbove
    +import androidx.window.WindowTestUtils.Companion.assumePlatformUOrAbove
    +import androidx.window.WindowTestUtils.Companion.runActionsAcrossActivityLifecycle
    +import org.junit.Assert.assertEquals
    +import org.junit.Rule
    +import org.junit.Test
    +import org.junit.runner.RunWith
    +
    +/** Tests for the [DensityCompatHelper] class. */
    +@LargeTest
    +@RunWith(AndroidJUnit4::class)
    +class DensityCompatHelperTest {
    +
    +    @get:Rule
    +    var activityScenarioRule: ActivityScenarioRule =
    +        ActivityScenarioRule(TestActivity::class.java)
    +
    +    @Test
    +    fun testDensityFromContext_beforeU() {
    +        assumePlatformBeforeU()
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val helper = DensityCompatHelper.getInstance()
    +            assertEquals(activity.resources.displayMetrics.density, helper.density(activity))
    +        }
    +    }
    +
    +    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +    @Test
    +    fun testDensityFromContext_UOrAbove() {
    +        assumePlatformUOrAbove()
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val wm = activity.getSystemService(WindowManager::class.java)
    +            val helper = DensityCompatHelper.getInstance()
    +            assertEquals(wm.currentWindowMetrics.density, helper.density(activity))
    +        }
    +    }
    +
    +    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +    @Test
    +    fun testDensityFromConfiguration_beforeU() {
    +        assumePlatformBeforeU()
    +        assumePlatformROrAbove()
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val helper = DensityCompatHelper.getInstance()
    +
    +            @Suppress("DEPRECATION")
    +            val fakeWindowMetrics = AndroidWindowMetrics(
    +                Rect(0, 0, 0, 0),
    +                WindowInsets.Builder().build(),
    +            )
    +
    +            val density = helper.density(activity.resources.configuration, fakeWindowMetrics)
    +            val expectedDensity = activity.resources.configuration.densityDpi.toFloat() /
    +                DisplayMetrics.DENSITY_DEFAULT
    +            assertEquals(expectedDensity, density)
    +        }
    +    }
    +
    +    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +    @Test
    +    fun testDensityFromWindowMetrics_UOrAbove() {
    +        assumePlatformUOrAbove()
    +        runActionsAcrossActivityLifecycle(activityScenarioRule, { }) { activity: TestActivity ->
    +            val helper = DensityCompatHelper.getInstance()
    +
    +            val fakeDensity = 123.456f
    +            val fakeWindowMetrics = AndroidWindowMetrics(
    +                Rect(0, 0, 0, 0),
    +                WindowInsets.Builder().build(),
    +                fakeDensity
    +            )
    +
    +            val density = helper.density(activity.resources.configuration, fakeWindowMetrics)
    +            assertEquals(fakeDensity, density)
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/WindowProperties.kt b/window/window/src/main/java/androidx/window/WindowProperties.kt
    index 405f79c..606b419 100644
    --- a/window/window/src/main/java/androidx/window/WindowProperties.kt
    +++ b/window/window/src/main/java/androidx/window/WindowProperties.kt
    
    @@ -119,7 +119,6 @@
          * 
          * ```
          */
    -    // TODO(b/274924641): Make property public
         const val PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED =
             "android.window.PROPERTY_COMPAT_ALLOW_IGNORING_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED"
     
    @@ -182,4 +181,97 @@
          */
         const val PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES =
             "android.window.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES"
    +
    +    /**
    +     * Application-level
    +     * [PackageManager][android.content.pm.PackageManager.Property] tag that
    +     * (when set to false) informs the system the app has opted out of the
    +     * user-facing aspect ratio compatibility override.
    +     *
    +     * The compatibility override enables device users to set the app's aspect
    +     * ratio or force the app to fill the display regardless of the aspect
    +     * ratio or orientation specified in the app manifest.
    +     *
    +     * The aspect ratio compatibility override is exposed to users in device
    +     * settings. A menu in device settings lists all apps that have not opted out of
    +     * the compatibility override. Users select apps from the menu and set the
    +     * app aspect ratio on a per-app basis. Typically, the menu is available
    +     * only on large screen devices.
    +     *
    +     * When users apply the aspect ratio override, the minimum aspect ratio
    +     * specified in the app manifest is overridden. If users choose a
    +     * full-screen aspect ratio, the orientation of the activity is forced to
    +     * [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER];
    +     * see [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE] to
    +     * disable the full-screen option only.
    +     *
    +     * The user override is intended to improve the app experience on devices
    +     * that have the ignore orientation request display setting enabled by OEMs
    +     * (enables compatibility mode for fixed orientation on Android 12 (API
    +     * level 31) or higher; see
    +     * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)
    +     * for more details).
    +     *
    +     * To opt out of the user aspect ratio compatibility override, add this property
    +     * to your app manifest and set the value to `false`. Your app will be excluded
    +     * from the list of apps in device settings, and users will not be able to
    +     * override the app's aspect ratio.
    +     *
    +     * Not setting this property at all, or setting this property to `true` has
    +     * no effect.
    +     *
    +     * **Syntax:**
    +     * ```
    +     * 
    +     *   
    +     *     android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE"
    +     *     android:value="false" />
    +     * 
    +     * ```
    +     */
    +    const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE =
    +        "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE";
    +
    +    /**
    +     * Application-level
    +     * [PackageManager][android.content.pm.PackageManager.Property] tag that
    +     * (when set to false) informs the system the app has opted out of the
    +     * full-screen option of the user aspect ratio compatibility override settings.
    +     * (For background information about the user aspect ratio compatibility override, see
    +     * [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE].)
    +     *
    +     * When users apply the full-screen compatibility override, the orientation
    +     * of the activity is forced to
    +     * [SCREEN_ORIENTATION_USER][android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER].
    +     *
    +     * The user override aims to improve the app experience on devices that have
    +     * the ignore orientation request display setting enabled by OEMs (enables
    +     * compatibility mode for fixed orientation on Android 12 (API level 31) or
    +     * higher; see
    +     * [Large screen compatibility mode](https://developer.android.com/guide/topics/large-screens/large-screen-compatibility-mode)
    +     * for more details).
    +     *
    +     * To opt out of the full-screen option of the user aspect ratio compatibility
    +     * override, add this property to your app manifest and set the value to
    +     * `false`. Your app will have full-screen option removed from the list of user
    +     * aspect ratio override options in device settings, and users will not be able
    +     * to apply full-screen override to your app.
    +     *
    +     * **Note:** If [PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE] is
    +     * `false`, this property has no effect.
    +     *
    +     * Not setting this property at all, or setting this property to `true`
    +     * has no effect.
    +     *
    +     * **Syntax:**
    +     * ```
    +     * 
    +     *   
    +     *     android:name="android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE"
    +     *     android:value="false" />
    +     * 
    +     * ```
    +     */
    +    const val PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE =
    +        "android.window.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE";
     }
    
    diff --git a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
    index 26f0a6d..60e8496 100644
    --- a/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
    +++ b/window/window/src/main/java/androidx/window/WindowSdkExtensions.kt
    
    @@ -17,6 +17,7 @@
     package androidx.window
     
     import androidx.annotation.IntRange
    +import androidx.annotation.RestrictTo
     import androidx.window.core.ExtensionsUtil
     
     /**
    @@ -31,7 +32,7 @@
      *
      * @sample androidx.window.samples.checkWindowSdkExtensionsVersion
      */
    -abstract class WindowSdkExtensions internal constructor() {
    +abstract class WindowSdkExtensions @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor() {
     
         /**
          * Reports the device's extension version
    @@ -55,6 +56,23 @@
             }
         }
     
    +    /**
    +     * Checks the [extensionVersion] and throws [UnsupportedOperationException] if the version is
    +     * not in the [range].
    +     *
    +     * This is useful to provide compatibility for APIs updated in 2+ but deprecated in latest
    +     * version.
    +     *
    +     * @param range the required extension range of the targeting API.
    +     * @throws UnsupportedOperationException if the required [range] is not satisfied.
    +     */
    +    internal fun requireExtensionVersion(range: kotlin.ranges.IntRange) {
    +        if (extensionVersion !in range) {
    +            throw UnsupportedOperationException("This API requires extension version " +
    +                "$range, but the device is on $extensionVersion")
    +        }
    +    }
    +
         companion object {
             /** Returns a [WindowSdkExtensions] instance. */
             @JvmStatic
    @@ -64,17 +82,20 @@
     
             private var decorator: WindowSdkExtensionsDecorator = EmptyDecoratorWindowSdk
     
    -        internal fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        fun overrideDecorator(overridingDecorator: WindowSdkExtensionsDecorator) {
                 decorator = overridingDecorator
             }
     
    -        internal fun reset() {
    +        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +        fun reset() {
                 decorator = EmptyDecoratorWindowSdk
             }
         }
     }
     
    -internal interface WindowSdkExtensionsDecorator {
    +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +interface WindowSdkExtensionsDecorator {
         /** Returns a [WindowSdkExtensions] instance. */
         fun decorate(windowSdkExtensions: WindowSdkExtensions): WindowSdkExtensions
     }
    
    diff --git a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
    index e0e9574..4b97c10 100644
    --- a/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
    +++ b/window/window/src/main/java/androidx/window/area/RearDisplayPresentationSessionPresenterImpl.kt
    
    @@ -18,6 +18,8 @@
     
     import android.content.Context
     import android.view.View
    +import android.view.Window
    +import androidx.window.area.utils.PresentationWindowCompatUtils
     import androidx.window.core.ExperimentalWindowApi
     import androidx.window.extensions.area.ExtensionWindowAreaPresentation
     import androidx.window.extensions.area.WindowAreaComponent
    @@ -25,11 +27,15 @@
     @ExperimentalWindowApi
     internal class RearDisplayPresentationSessionPresenterImpl(
         private val windowAreaComponent: WindowAreaComponent,
    -    private val presentation: ExtensionWindowAreaPresentation
    +    private val presentation: ExtensionWindowAreaPresentation,
    +    vendorApiLevel: Int
     ) : WindowAreaSessionPresenter {
     
         override val context: Context = presentation.presentationContext
     
    +    override val window: Window? =
    +        if (vendorApiLevel >= 4) presentation.window else
    +            PresentationWindowCompatUtils.getWindowBeforeVendorApiLevel4(presentation)
         override fun setContentView(view: View) {
             presentation.setPresentationView(view)
         }
    
    diff --git a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
    index 8eb9696..13a16ca 100644
    --- a/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
    +++ b/window/window/src/main/java/androidx/window/area/SafeWindowAreaComponentProvider.kt
    
    @@ -15,6 +15,7 @@
      */
     package androidx.window.area
     
    +import android.os.Build
     import androidx.window.SafeWindowExtensionsProvider
     import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid
     import androidx.window.area.reflectionguard.WindowAreaComponentValidator.isExtensionWindowAreaStatusValid
    @@ -41,15 +42,29 @@
                     if (
                         windowExtensions != null &&
                         isWindowAreaProviderValid(windowExtensions) &&
    +                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
                         isWindowAreaComponentValid(
                             windowAreaComponentClass, ExtensionsUtil.safeVendorApiLevel
                         ) &&
                         isExtensionWindowAreaStatusValid(
                             extensionWindowAreaStatusClass, ExtensionsUtil.safeVendorApiLevel
                         ) &&
    -                    isValidExtensionWindowPresentation()
    -                ) windowExtensions.windowAreaComponent else null
    -            } catch (e: Exception) {
    +                    isExtensionWindowAreaPresentationValid(
    +                        extensionWindowAreaPresentationClass, ExtensionsUtil.safeVendorApiLevel
    +                    )
    +                ) {
    +                    if (ExtensionsUtil.safeVendorApiLevel >= 3) {
    +                        windowExtensions.windowAreaComponent
    +                    } else {
    +                        // Use reflection to get WindowAreaComponent if the WindowExtensions AAR
    +                        // on device does not have the #getWindowAreaComponent method.
    +                        windowExtensions.javaClass.getMethod("getWindowAreaComponent")
    +                            .invoke(windowExtensions) as WindowAreaComponent
    +                    }
    +                } else {
    +                    null
    +                }
    +            } catch (e: Throwable) {
                     null
                 }
             }
    @@ -65,14 +80,6 @@
             }
         }
     
    -    private fun isValidExtensionWindowPresentation(): Boolean {
    -        // Not required for API Level 2 or below
    -        return ExtensionsUtil.safeVendorApiLevel <= 2 ||
    -            isExtensionWindowAreaPresentationValid(
    -                extensionWindowAreaPresentationClass, ExtensionsUtil.safeVendorApiLevel
    -            )
    -    }
    -
         private val windowAreaComponentClass: Class<*>
             get() {
                 return loader.loadClass(WindowExtensionsConstants.WINDOW_AREA_COMPONENT_CLASS)
    
    diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
    index d671726..dbf74ac 100644
    --- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
    +++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
    
    @@ -23,7 +23,7 @@
     import androidx.annotation.RestrictTo
     import androidx.window.WindowSdkExtensions
     import androidx.window.area.WindowAreaInfo.Type.Companion.TYPE_REAR_FACING
    -import androidx.window.area.utils.DeviceUtils
    +import androidx.window.area.utils.DeviceMetricsCompatUtils
     import androidx.window.core.BuildConfig
     import androidx.window.core.ExperimentalWindowApi
     import androidx.window.core.ExtensionsUtil
    @@ -150,18 +150,17 @@
                     }
                     null
                 }
    +
                 val deviceSupported = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
                     windowAreaComponentExtensions != null &&
    -                (ExtensionsUtil.safeVendorApiLevel >= 3 || DeviceUtils.hasDeviceMetrics(
    -                    Build.MANUFACTURER,
    -                    Build.MODEL
    -                ))
    +                (ExtensionsUtil.safeVendorApiLevel >= 3 ||
    +                    DeviceMetricsCompatUtils.hasDeviceMetrics())
     
                 val controller =
                     if (deviceSupported) {
                         WindowAreaControllerImpl(
    -                        windowAreaComponentExtensions!!,
    -                        ExtensionsUtil.safeVendorApiLevel
    +                        windowAreaComponent = windowAreaComponentExtensions!!,
    +                        presentationSupported = ExtensionsUtil.safeVendorApiLevel >= 3,
                         )
                     } else {
                         EmptyWindowAreaControllerImpl()
    
    diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
    index 8d21d67..d15e94e 100644
    --- a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
    +++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
    
    @@ -21,14 +21,16 @@
     import android.os.Build
     import android.util.Log
     import androidx.annotation.RequiresApi
    +import androidx.window.RequiresWindowSdkExtension
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_AVAILABLE
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNKNOWN
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
     import androidx.window.area.adapter.WindowAreaAdapter
    -import androidx.window.area.utils.DeviceUtils
    +import androidx.window.area.utils.DeviceMetricsCompatUtils
     import androidx.window.core.BuildConfig
     import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.core.ExtensionsUtil
     import androidx.window.core.VerificationMode
     import androidx.window.extensions.area.ExtensionWindowAreaStatus
     import androidx.window.extensions.area.WindowAreaComponent
    @@ -58,10 +60,11 @@
      * this functionality.
      */
     @ExperimentalWindowApi
    +@RequiresWindowSdkExtension(2)
     @RequiresApi(Build.VERSION_CODES.Q)
     internal class WindowAreaControllerImpl(
         private val windowAreaComponent: WindowAreaComponent,
    -    private val vendorApiLevel: Int
    +    private val presentationSupported: Boolean,
     ) : WindowAreaController {
     
         private lateinit var rearDisplaySessionConsumer: Consumer
    @@ -89,7 +92,8 @@
                         }
     
                     windowAreaComponent.addRearDisplayStatusListener(rearDisplayListener)
    -                if (vendorApiLevel > 2) {
    +
    +                if (presentationSupported) {
                         windowAreaComponent.addRearDisplayPresentationStatusListener(
                             rearDisplayPresentationListener
                         )
    @@ -97,7 +101,8 @@
     
                     awaitClose {
                         windowAreaComponent.removeRearDisplayStatusListener(rearDisplayListener)
    -                    if (vendorApiLevel > 2) {
    +
    +                    if (presentationSupported) {
                             windowAreaComponent.removeRearDisplayPresentationStatusListener(
                                 rearDisplayPresentationListener
                             )
    @@ -109,12 +114,12 @@
         private fun updateRearDisplayAvailability(
             status: @WindowAreaComponent.WindowAreaStatus Int
         ) {
    -        val windowMetrics = if (vendorApiLevel >= 3) {
    +        val windowMetrics = if (presentationSupported) {
                 WindowMetricsCalculator.fromDisplayMetrics(
                     displayMetrics = windowAreaComponent.rearDisplayMetrics
                 )
             } else {
    -            val displayMetrics = DeviceUtils.getRearDisplayMetrics(Build.MANUFACTURER, Build.MODEL)
    +            val displayMetrics = DeviceMetricsCompatUtils.getDeviceMetrics()?.rearDisplayMetrics
                 if (displayMetrics != null) {
                     WindowMetricsCalculator.fromDisplayMetrics(
                         displayMetrics = displayMetrics
    @@ -389,7 +394,8 @@
                                 windowAreaPresentationSessionCallback.onSessionStarted(
                                     RearDisplayPresentationSessionPresenterImpl(
                                         windowAreaComponent,
    -                                    windowAreaComponent.rearDisplayPresentation!!
    +                                    windowAreaComponent.rearDisplayPresentation!!,
    +                                    ExtensionsUtil.safeVendorApiLevel
                                     )
                                 )
                             }
    
    diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
    index 4e031cf3..f3e5e1c 100644
    --- a/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
    +++ b/window/window/src/main/java/androidx/window/area/WindowAreaInfo.kt
    
    @@ -22,6 +22,7 @@
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_ACTIVE
     import androidx.window.area.WindowAreaCapability.Status.Companion.WINDOW_AREA_STATUS_UNSUPPORTED
     import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.core.ExtensionsUtil
     import androidx.window.extensions.area.WindowAreaComponent
     import androidx.window.layout.WindowMetrics
     
    @@ -92,7 +93,8 @@
                 OPERATION_PRESENT_ON_AREA ->
                     RearDisplayPresentationSessionPresenterImpl(
                         windowAreaComponent,
    -                    windowAreaComponent.rearDisplayPresentation!!
    +                    windowAreaComponent.rearDisplayPresentation!!,
    +                    ExtensionsUtil.safeVendorApiLevel
                     )
                 else -> {
                     throw IllegalArgumentException("Invalid operation provided")
    
    diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
    index 4cc05a9..fbf967a 100644
    --- a/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
    +++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionPresenter.kt
    
    @@ -18,6 +18,7 @@
     
     import android.content.Context
     import android.view.View
    +import android.view.Window
     import androidx.window.core.ExperimentalWindowApi
     
     /**
    @@ -35,6 +36,15 @@
         val context: Context
     
         /**
    +     * Returns the [Window] associated with the active presentation window area or null if there is
    +     * no [Window] currently active. This could occur if the presenter has already been dismissed,
    +     * and there is no expectation that the [Window] would become non-null at a later point.
    +     * This API can be used to directly access parts of the [Window] API that are not available
    +     * through the [Context] provided.
    +     */
    +    val window: Window?
    +
    +    /**
          * Sets a [View] to show on a window area. After setting the view the system can turn on the
          * corresponding display and start showing content.
          */
    
    diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
    index d48d2ab..7390726 100644
    --- a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
    +++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
    
    @@ -26,38 +26,45 @@
     internal object WindowAreaComponentValidator {
     
         internal fun isWindowAreaComponentValid(windowAreaComponent: Class<*>, apiLevel: Int): Boolean {
    -        return when {
    +        val isWindowAreaComponentValid: Boolean = when {
                 apiLevel <= 1 -> false
    +
                 apiLevel == 2 -> validateImplementation(
                     windowAreaComponent, WindowAreaComponentApi2Requirements::class.java
                 )
    +
                 else -> validateImplementation(
                     windowAreaComponent, WindowAreaComponentApi3Requirements::class.java
                 )
             }
    +        return isWindowAreaComponentValid
         }
     
         internal fun isExtensionWindowAreaStatusValid(
             extensionWindowAreaStatus: Class<*>,
             apiLevel: Int
         ): Boolean {
    -        return when {
    -            apiLevel <= 1 -> false
    +        val isExtensionWindowAreaStatusValid: Boolean = when {
    +            apiLevel <= 2 -> true
    +
                 else -> validateImplementation(
                     extensionWindowAreaStatus, ExtensionWindowAreaStatusRequirements::class.java
                 )
             }
    +        return isExtensionWindowAreaStatusValid
         }
     
         internal fun isExtensionWindowAreaPresentationValid(
             extensionWindowAreaPresentation: Class<*>,
             apiLevel: Int
         ): Boolean {
    -        return when {
    -            apiLevel <= 2 -> false
    +        val isExtensionWindowAreaPresentationValid: Boolean = when {
    +            apiLevel <= 2 -> true
    +
                 else -> validateImplementation(
                     extensionWindowAreaPresentation, ExtensionWindowAreaPresentation::class.java
                 )
             }
    +        return isExtensionWindowAreaPresentationValid
         }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
    index 75a2db6..37078fd 100644
    --- a/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
    +++ b/window/window/src/main/java/androidx/window/area/utils/DeviceMetrics.kt
    
    @@ -22,26 +22,26 @@
      * Data class holding metrics about a specific device.
      */
     internal class DeviceMetrics(
    -    val manufacturer: String,
    +    val brand: String,
         val model: String,
         val rearDisplayMetrics: DisplayMetrics
     ) {
         override fun equals(other: Any?): Boolean {
             return other is DeviceMetrics &&
    -            manufacturer == other.manufacturer &&
    +            brand == other.brand &&
                 model == other.model &&
                 rearDisplayMetrics.equals(other.rearDisplayMetrics)
         }
     
         override fun hashCode(): Int {
    -        var result = manufacturer.hashCode()
    +        var result = brand.hashCode()
             result = 31 * result + model.hashCode()
             result = 31 * result + rearDisplayMetrics.hashCode()
             return result
         }
     
         override fun toString(): String {
    -        return "DeviceMetrics{ Manufacturer: $manufacturer, model: $model, " +
    +        return "DeviceMetrics{ Brand: $brand, model: $model, " +
                 "Rear display metrics: $rearDisplayMetrics }"
         }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceMetricsCompatUtils.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceMetricsCompatUtils.kt
    new file mode 100644
    index 0000000..b130f2d
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/area/utils/DeviceMetricsCompatUtils.kt
    
    @@ -0,0 +1,53 @@
    +/*
    + * 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 androidx.window.area.utils
    +
    +import android.os.Build
    +import android.util.DisplayMetrics
    +import androidx.window.area.WindowAreaController
    +import androidx.window.extensions.area.WindowAreaComponent
    +
    +/**
    + * Object to provide util methods for non-standard behaviors around device metrics that are needed
    + * or provided through the [WindowAreaController] API's that may exist on certain devices or certain
    + * vendor API levels. This currently includes the rear display metrics for devices that support
    + * WindowArea features, but do not currently support the [WindowAreaComponent.getRearDisplayMetrics]
    + * method. This object can expand to include any differences that have to be taken into account that
    + * vary from the standard behavior.
    + */
    +internal object DeviceMetricsCompatUtils {
    +
    +    private val deviceMetricsList = listOf(DeviceMetrics("google", "pixel fold",
    +        DisplayMetrics().apply {
    +            widthPixels = 1080
    +            heightPixels = 2092
    +            density = 2.625f
    +            densityDpi = DisplayMetrics.DENSITY_420
    +        }
    +    ))
    +
    +    fun hasDeviceMetrics(): Boolean {
    +        return getDeviceMetrics() != null
    +    }
    +
    +    fun getDeviceMetrics(): DeviceMetrics? {
    +        return deviceMetricsList.firstOrNull { deviceMetrics ->
    +            deviceMetrics.brand.equals(Build.BRAND, ignoreCase = true) &&
    +            deviceMetrics.model.equals(Build.MODEL, ignoreCase = true)
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt b/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
    deleted file mode 100644
    index e72194a..0000000
    --- a/window/window/src/main/java/androidx/window/area/utils/DeviceUtils.kt
    +++ /dev/null
    
    @@ -1,49 +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 androidx.window.area.utils
    -
    -import android.util.DisplayMetrics
    -import java.util.Locale
    -
    -/**
    - * Utility object to provide information about specific devices that may not be available
    - * through the extensions API at a certain vendor API level
    - */
    -internal object DeviceUtils {
    -
    -    private val deviceList = listOf(DeviceMetrics("google", "pixel fold",
    -        DisplayMetrics().apply {
    -            widthPixels = 1080
    -            heightPixels = 2092
    -            density = 2.625f
    -            densityDpi = 420 }
    -        ))
    -
    -    internal fun hasDeviceMetrics(manufacturer: String, model: String): Boolean {
    -        return deviceList.any {
    -            it.manufacturer == manufacturer.lowercase(Locale.US) &&
    -                it.model == model.lowercase(Locale.US)
    -        }
    -    }
    -
    -    internal fun getRearDisplayMetrics(manufacturer: String, model: String): DisplayMetrics? {
    -        return deviceList.firstOrNull {
    -            it.manufacturer == manufacturer.lowercase(Locale.US) &&
    -                it.model == model.lowercase(Locale.US)
    -        }?.rearDisplayMetrics
    -    }
    -}
    
    diff --git a/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt b/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt
    new file mode 100644
    index 0000000..eaa2551
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/area/utils/PresentationWindowCompatUtils.kt
    
    @@ -0,0 +1,41 @@
    +/*
    + * 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 androidx.window.area.utils
    +
    +import android.annotation.SuppressLint
    +import android.view.Window
    +import androidx.window.extensions.area.ExtensionWindowAreaPresentation
    +import java.lang.reflect.Method
    +
    +internal object PresentationWindowCompatUtils {
    +
    +    // We perform our own extensions vendor API level check at the call-site
    +    @SuppressLint("BanUncheckedReflection")
    +    fun getWindowBeforeVendorApiLevel4(
    +        extensionPresentation: ExtensionWindowAreaPresentation
    +    ): Window? {
    +        val getWindowMethod = getWindowMethod(extensionPresentation)
    +        return if (getWindowMethod == null) null else
    +            (getWindowMethod.invoke(extensionPresentation) as Window?)
    +    }
    +
    +    private fun getWindowMethod(extensionPresentation: ExtensionWindowAreaPresentation): Method? {
    +        return extensionPresentation.javaClass.methods.firstOrNull { method: Method? ->
    +            method?.name == "getWindow" && method.returnType == android.view.Window::class.java
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/core/Bounds.kt b/window/window/src/main/java/androidx/window/core/Bounds.kt
    index 630c188..8c2bfa3 100644
    --- a/window/window/src/main/java/androidx/window/core/Bounds.kt
    +++ b/window/window/src/main/java/androidx/window/core/Bounds.kt
    
    @@ -28,10 +28,10 @@
      * not contain any behavior or calculations.
      */
     internal class Bounds(
    -    public val left: Int,
    -    public val top: Int,
    -    public val right: Int,
    -    public val bottom: Int
    +    val left: Int,
    +    val top: Int,
    +    val right: Int,
    +    val bottom: Int,
     ) {
         constructor(rect: Rect) : this(rect.left, rect.top, rect.right, rect.bottom)
     
    @@ -98,4 +98,8 @@
             result = 31 * result + bottom
             return result
         }
    +
    +    companion object {
    +        val EMPTY_BOUNDS = Bounds(0, 0, 0, 0)
    +    }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
    index 0d06481..afc9eec 100644
    --- a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingController.kt
    
    @@ -17,11 +17,14 @@
     package androidx.window.embedding
     
     import android.app.Activity
    -import android.app.ActivityOptions
     import android.content.Context
    -import android.os.IBinder
    +import android.os.Bundle
    +import androidx.core.util.Consumer
     import androidx.window.RequiresWindowSdkExtension
    -import androidx.window.core.ExperimentalWindowApi
    +import androidx.window.WindowSdkExtensions
    +import kotlinx.coroutines.channels.awaitClose
    +import kotlinx.coroutines.flow.Flow
    +import kotlinx.coroutines.flow.callbackFlow
     
     /**
      * The controller that allows checking the current [Activity] embedding status.
    @@ -33,7 +36,6 @@
          *
          * @param activity the [Activity] to check.
          */
    -    // TODO(b/204399167) Migrate to a Flow
         fun isActivityEmbedded(activity: Activity): Boolean =
             backend.isActivityEmbedded(activity)
     
    @@ -42,28 +44,141 @@
          * embedding container and associated with a [SplitInfo]. Returns `null` if there is no such
          * [ActivityStack].
          *
    +     * Started from [WindowSdkExtensions.extensionVersion] 5, this method can also obtain
    +     * standalone [ActivityStack], which is not associated with any [SplitInfo]. For example,
    +     * an [ActivityStack] launched with [ActivityRule.alwaysExpand], or an overlay [ActivityStack]
    +     * launched by [setLaunchingActivityStack] with [OverlayCreateParams].
    +     *
          * @param activity The [Activity] to check.
          * @return the [ActivityStack] that this [activity] is part of, or `null` if there is no such
          * [ActivityStack].
          */
    -    @ExperimentalWindowApi
         fun getActivityStack(activity: Activity): ActivityStack? =
             backend.getActivityStack(activity)
     
         /**
    -     * Sets the launching [ActivityStack] to the given [android.app.ActivityOptions].
    +     * Sets the launching [ActivityStack] to the given [Bundle].
          *
    -     * @param options The [android.app.ActivityOptions] to be updated.
    -     * @param token The token of the [ActivityStack] to be set.
    +     * Apps can launch an [Activity] into the [ActivityStack] by [Activity.startActivity].
    +     *
    +     * @param options the [Bundle] to be updated.
    +     * @param activityStack the [ActivityStack] to be set.
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    internal fun setLaunchingActivityStack(
    +        options: Bundle,
    +        activityStack: ActivityStack
    +    ): Bundle = backend.setLaunchingActivityStack(options, activityStack)
    +
    +    /**
    +     * Finishes a set of [activityStacks][ActivityStack] from the lowest to the highest z-order
    +     * regardless of the order of `activityStack` passed in the input parameter.
    +     *
    +     * If a remaining activityStack from a split participates in other splits with
    +     * other activityStacks, the remaining activityStack might split with other activityStacks.
    +     * For example, if activityStack A splits with activityStack B and C, and activityStack C covers
    +     * activityStack B, finishing activityStack C might make the split of activityStack A and B
    +     * show.
    +     *
    +     * If all split-associated activityStacks are finished, the remaining activityStack will
    +     * be expanded to fill the parent task container. This is useful to expand the primary
    +     * container as the sample linked below shows.
    +     *
    +     * **Note** that it's caller's responsibility to check whether this API is supported by checking
    +     * [WindowSdkExtensions.extensionVersion] is greater than or equal to 5. If not, an alternative
    +     * approach to finishing all containers above a particular activity can be to launch it again
    +     * with flag [android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP].
    +     *
    +     * @param activityStacks The set of [ActivityStack] to be finished.
    +     * @throws UnsupportedOperationException if extension version is less than 5.
    +     * @sample androidx.window.samples.embedding.expandPrimaryContainer
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    fun finishActivityStacks(activityStacks: Set) {
    +        backend.finishActivityStacks(activityStacks)
    +    }
    +
    +    /**
    +     * Sets the [EmbeddingConfiguration] of the Activity Embedding environment that defines how the
    +     * embedded Activities behaves.
    +     *
    +     * The [EmbeddingConfiguration] can be supported only if the vendor API level of the target
    +     * device is equals or higher than required API level. Otherwise, it would be no-op when setting
    +     * the [EmbeddingConfiguration] on a target device that has lower API level.
    +     *
    +     * In addition, the existing configuration in the library won't be overwritten if the properties
    +     * of the given [embeddingConfiguration] are undefined. Only the configuration properties that
    +     * are explicitly set will be updated.
    +     *
    +     * **Note** that it is recommended to be configured in the [androidx.startup.Initializer] or
    +     * [android.app.Application.onCreate], so that the [EmbeddingConfiguration] is applied early
    +     * in the application startup, before any activities complete initialization. The
    +     * [EmbeddingConfiguration] updates afterward may or may not apply to already running
    +     * activities.
    +     *
    +     * @param embeddingConfiguration The [EmbeddingConfiguration]
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    fun setEmbeddingConfiguration(embeddingConfiguration: EmbeddingConfiguration) {
    +        backend.setEmbeddingConfiguration(embeddingConfiguration)
    +    }
    +
    +    /**
    +     * Triggers calculator functions set through [SplitController.setSplitAttributesCalculator] and
    +     * [OverlayController.setOverlayAttributesCalculator] to update attributes for visible
    +     * [activityStacks][ActivityStack].
    +     *
    +     * This method can be used when the application wants to update the embedding presentation based
    +     * on the application state.
    +     *
    +     * This method is not needed for changes that are driven by window and device state changes or
    +     * new activity starts, because those will invoke the calculator functions
    +     * automatically.
    +     *
    +     * Visible [activityStacks][ActivityStack] are usually the last element of [SplitInfo]
    +     * list which was received from the callback registered in [SplitController.splitInfoList] and
    +     * an active overlay [ActivityStack] if exists.
    +     *
    +     * The call will be no-op if there is no visible [activityStacks][ActivityStack] or there's no
    +     * calculator set.
    +     *
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    +     *                                       is less than 3.
    +     * @see androidx.window.embedding.OverlayController.setOverlayAttributesCalculator
    +     * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator
          */
         @RequiresWindowSdkExtension(3)
    -    internal fun setLaunchingActivityStack(
    -        options: ActivityOptions,
    -        token: IBinder
    -    ): ActivityOptions {
    -        return backend.setLaunchingActivityStack(options, token)
    +    fun invalidateVisibleActivityStacks() {
    +        backend.invalidateVisibleActivityStacks()
         }
     
    +    /**
    +     * A [Flow] of [EmbeddedActivityWindowInfo] that reports the change to the embedded window
    +     * related info of the [activity].
    +     *
    +     * The [Flow] will immediately be invoked with the latest value upon registration if the
    +     * [activity] is currently embedded as [EmbeddedActivityWindowInfo.isEmbedded] is `true`.
    +     *
    +     * When the [activity] is embedded, the [Flow] will be invoked when [EmbeddedActivityWindowInfo]
    +     * is changed.
    +     * When the [activity] is not embedded, the [Flow] will not be triggered unless the [activity]
    +     * is becoming non-embedded from embedded.
    +     *
    +     * Note that this API is only supported on the device with
    +     * [WindowSdkExtensions.extensionVersion] equal to or larger than 6.
    +     * If [WindowSdkExtensions.extensionVersion] is less than 6, this [Flow] will not be invoked.
    +     *
    +     * @param activity the [Activity] that is interested in getting the embedded window info.
    +     * @return a [Flow] of [EmbeddedActivityWindowInfo] of the [activity].
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun embeddedActivityWindowInfo(activity: Activity): Flow =
    +        callbackFlow {
    +            val callback = Consumer { info: EmbeddedActivityWindowInfo -> trySend(info) }
    +            backend.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)
    +            awaitClose { backend.removeEmbeddedActivityWindowInfoCallbackForActivity(callback) }
    +        }
    +
         companion object {
             /**
              * Obtains an instance of [ActivityEmbeddingController].
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
    new file mode 100644
    index 0000000..1447f18
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptions.kt
    
    @@ -0,0 +1,107 @@
    +/*
    + * 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.
    + */
    +@file:JvmName("ActivityEmbeddingOptions")
    +
    +package androidx.window.embedding
    +
    +import android.app.Activity
    +import android.app.ActivityOptions
    +import android.content.Context
    +import android.os.Bundle
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +
    +/**
    + * Sets the target launching [ActivityStack] to the given [Bundle].
    + *
    + * The [Bundle] then could be used to launch an [Activity] to the top of the [ActivityStack] through
    + * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be
    + * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle],
    + * it's suggested to use the bundle to call this method.
    + *
    + * It is suggested to use a visible [ActivityStack] reported by [SplitController.splitInfoList]
    + * or [OverlayController.overlayInfo], or the launching activity will be launched on the default
    + * target if the [activityStack] no longer exists in the host task. The default target could be
    + * the top of the visible split's secondary [ActivityStack], or the top of the host task.
    + *
    + * Below samples are use cases to specify the launching [ActivityStack].
    + *
    + * @sample androidx.window.samples.embedding.launchingOnPrimaryActivityStack
    + * @sample androidx.window.samples.embedding.launchingOnOverlayActivityStack
    + *
    + * @param context The [android.content.Context] that is going to be used for launching
    + * activity with this [Bundle], which is usually be the [android.app.Activity] of the app that
    + * hosts the task.
    + * @param activityStack The target [ActivityStack] for launching.
    + * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 5.
    + */
    +@RequiresWindowSdkExtension(5)
    +fun Bundle.setLaunchingActivityStack(
    +    context: Context,
    +    activityStack: ActivityStack
    +): Bundle = ActivityEmbeddingController.getInstance(context)
    +        .setLaunchingActivityStack(this, activityStack)
    +
    +/**
    + * Puts [OverlayCreateParams] to [Bundle] to create a singleton-per-task overlay [ActivityStack].
    + *
    + * The [Bundle] then could be used to launch an [Activity] to the [ActivityStack] through
    + * [Activity.startActivity]. If there's a bundle used for customizing how the [Activity] should be
    + * started by [ActivityOptions.toBundle] or [androidx.core.app.ActivityOptionsCompat.toBundle],
    + * it's suggested to use the bundle to call this method.
    + *
    + * Below sample shows how to launch an overlay [ActivityStack].
    + *
    + * If there's an existing overlay [ActivityStack] shown, the existing overlay container may be
    + * dismissed or updated based on [OverlayCreateParams.tag] and [activity] because of following
    + * constraints:
    + *   1. A task can hold only one overlay container at most.
    + *   2. An overlay [ActivityStack] tag is unique per process.
    + *
    + * Belows are possible scenarios:
    + *
    + * 1. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in the same
    + *   task as [activity], the overlay container's [OverlayAttributes]
    + *   will be updated to [OverlayCreateParams.overlayAttributes], and the activity will be launched
    + *   on the top of the overlay [ActivityStack].
    + *
    + * 2. If there's an overlay container with different `tag` from [OverlayCreateParams.tag] in the
    + *   same task as [activity], the existing overlay container will be dismissed, and a new overlay
    + *   container will be launched with the new [OverlayCreateParams].
    + *
    + * 3. If there's an overlay container with the same `tag` as [OverlayCreateParams.tag] in a
    + *   different task from [activity], the existing overlay container in the other task will be
    + *   dismissed, and a new overlay container will be launched in the task of [activity].
    + *
    + * Note that the second and the third scenarios may happen at the same time if
    + * [activity]'s task holds an overlay container and [OverlayCreateParams.tag] matches an overlay
    + * container in a different task.
    + *
    + * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample
    + *
    + * @param activity The [Activity] that is going to be used for launching activity with this
    + * [ActivityOptions], which is usually be the [Activity] of the app that hosts the task.
    + * @param overlayCreateParams The parameter container to create an overlay [ActivityStack]
    + * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than 6.
    + */
    +@RequiresWindowSdkExtension(6)
    +fun Bundle.setOverlayCreateParams(
    +    activity: Activity,
    +    overlayCreateParams: OverlayCreateParams
    +): Bundle = OverlayController.getInstance(activity).setOverlayCreateParams(
    +    this,
    +    overlayCreateParams
    +)
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt
    new file mode 100644
    index 0000000..595d413
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/ActivityEmbeddingOptionsImpl.kt
    
    @@ -0,0 +1,252 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.os.Bundle
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.embedding.EmbeddingBounds.Dimension
    +import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED
    +import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE
    +import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_ACTIVITY_STACK_TOKEN
    +import androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG
    +import androidx.window.extensions.embedding.ActivityStack.Token
    +
    +/**
    + * The implementation of ActivityEmbeddingOptions in WM Jetpack which uses constants defined in
    + * [androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties] and this object.
    + */
    +internal object ActivityEmbeddingOptionsImpl {
    +
    +    /**
    +     * Key of [EmbeddingBounds].
    +     *
    +     * Type: [Bundle]
    +     *
    +     * Properties:
    +     *
    +     * |Key|Type|Property|
    +     * |---|----|----|
    +     * |[KEY_EMBEDDING_BOUNDS_ALIGNMENT]|[Int]|[EmbeddingBounds.alignment]|
    +     * |[KEY_EMBEDDING_BOUNDS_WIDTH]|[Bundle]|[EmbeddingBounds.width]|
    +     * |[KEY_EMBEDDING_BOUNDS_HEIGHT]|[Bundle]|[EmbeddingBounds.height]|
    +     */
    +    private const val KEY_EMBEDDING_BOUNDS = "androidx.window.embedding.EmbeddingBounds"
    +
    +    /**
    +     * Key of [EmbeddingBounds.alignment].
    +     *
    +     * Type: [Int]
    +     *
    +     * Valid values are:
    +     * - 0: [EmbeddingBounds.Alignment.ALIGN_TOP]
    +     * - 1: [EmbeddingBounds.Alignment.ALIGN_LEFT]
    +     * - 2: [EmbeddingBounds.Alignment.ALIGN_BOTTOM]
    +     * - 3: [EmbeddingBounds.Alignment.ALIGN_RIGHT]
    +     */
    +    private const val KEY_EMBEDDING_BOUNDS_ALIGNMENT =
    +        "androidx.window.embedding.EmbeddingBounds.alignment"
    +
    +    /**
    +     * Key of [EmbeddingBounds.width].
    +     *
    +     * Type: [Bundle] with [putDimension]
    +     *
    +     * Properties:
    +     *
    +     * |Key|Type|Property|
    +     * |---|----|----|
    +     * |[KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]|[String]| The dimension type |
    +     * |[KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]|[Int], [Float]| The dimension value |
    +     */
    +    private const val KEY_EMBEDDING_BOUNDS_WIDTH = "androidx.window.embedding.EmbeddingBounds.width"
    +
    +    /**
    +     * Key of [EmbeddingBounds.width].
    +     *
    +     * Type: [Bundle] with [putDimension]
    +     *
    +     * Properties:
    +     *
    +     * |Key|Type|Property|
    +     * |---|----|----|
    +     * |[KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]|[String]| The dimension type |
    +     * |[KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]|[Int], [Float]| The dimension value |
    +     */
    +    private const val KEY_EMBEDDING_BOUNDS_HEIGHT =
    +        "androidx.window.embedding.EmbeddingBounds.height"
    +
    +    /**
    +     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates
    +     * the [Dimension] is [DIMENSION_EXPANDED].
    +     */
    +    private const val DIMENSION_TYPE_EXPANDED = "expanded"
    +
    +    /**
    +     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates
    +     * the [Dimension] is [DIMENSION_HINGE].
    +     */
    +    private const val DIMENSION_TYPE_HINGE = "hinge"
    +
    +    /**
    +     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates
    +     * the [Dimension] is from [Dimension.ratio]. If this type is used,
    +     * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Float] value.
    +     */
    +    private const val DIMENSION_TYPE_RATIO = "ratio"
    +
    +    /**
    +     * A [Dimension] type passed with [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE], which indicates
    +     * the [Dimension] is from [Dimension.pixel]. If this type is used,
    +     * [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE] should also be specified with a [Int] value.
    +     */
    +    private const val DIMENSION_TYPE_PIXEL = "pixel"
    +
    +    /**
    +     * Key of [EmbeddingBounds.Dimension] type.
    +     *
    +     * Type: [String]
    +     *
    +     * Valid values are:
    +     * - [DIMENSION_TYPE_EXPANDED]: [DIMENSION_EXPANDED]
    +     * - [DIMENSION_TYPE_HINGE]: [DIMENSION_HINGE]
    +     * - [DIMENSION_TYPE_RATIO]: [Dimension.ratio]
    +     * - [DIMENSION_TYPE_PIXEL]: [Dimension.pixel]
    +     */
    +    private const val KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE =
    +        "androidx.window.embedding.EmbeddingBounds.dimension_type"
    +
    +    /**
    +     * Key of [EmbeddingBounds.Dimension] value.
    +     *
    +     * Type: [Float] or [Int]
    +     *
    +     * The value passed in [Dimension.pixel] or [Dimension.ratio]:
    +     * - Accept [Float] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_RATIO]
    +     * - Accept [Int] if [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE] is [DIMENSION_TYPE_PIXEL]
    +     */
    +    private const val KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE =
    +        "androidx.window.embedding.EmbeddingBounds.dimension_value"
    +
    +    /**
    +     * Puts [OverlayCreateParams] information to [android.app.ActivityOptions] bundle to launch
    +     * the overlay container
    +     *
    +     * @param overlayCreateParams The [OverlayCreateParams] to launch the overlay container
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    internal fun setOverlayCreateParams(
    +        options: Bundle,
    +        overlayCreateParams: OverlayCreateParams,
    +    ) {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
    +
    +        options.putString(KEY_OVERLAY_TAG, overlayCreateParams.tag)
    +        options.putEmbeddingBounds(overlayCreateParams.overlayAttributes.bounds)
    +    }
    +
    +    /**
    +     * Puts [EmbeddingBounds] information into a bundle for tracking.
    +     */
    +    private fun Bundle.putEmbeddingBounds(embeddingBounds: EmbeddingBounds) {
    +        putBundle(
    +            KEY_EMBEDDING_BOUNDS,
    +            Bundle().apply {
    +                putInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT, embeddingBounds.alignment.value)
    +                putDimension(KEY_EMBEDDING_BOUNDS_WIDTH, embeddingBounds.width)
    +                putDimension(KEY_EMBEDDING_BOUNDS_HEIGHT, embeddingBounds.height)
    +            }
    +        )
    +    }
    +
    +    internal fun Bundle.getOverlayAttributes(): OverlayAttributes? {
    +        val embeddingBounds = getEmbeddingBounds() ?: return null
    +        return OverlayAttributes(embeddingBounds)
    +    }
    +
    +    private fun Bundle.getEmbeddingBounds(): EmbeddingBounds? {
    +        val embeddingBoundsBundle = getBundle(KEY_EMBEDDING_BOUNDS) ?: return null
    +        return EmbeddingBounds(
    +            EmbeddingBounds.Alignment(embeddingBoundsBundle.getInt(KEY_EMBEDDING_BOUNDS_ALIGNMENT)),
    +            embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_WIDTH),
    +            embeddingBoundsBundle.getDimension(KEY_EMBEDDING_BOUNDS_HEIGHT)
    +        )
    +    }
    +
    +    /**
    +     * Retrieves [EmbeddingBounds.Dimension] value from bundle with given [key].
    +     *
    +     * See [putDimension] for the data structure of [EmbeddingBounds.Dimension] as bundle
    +     */
    +    private fun Bundle.getDimension(key: String): Dimension {
    +        val dimensionBundle = getBundle(key)!!
    +        return when (val type = dimensionBundle.getString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE)) {
    +            DIMENSION_TYPE_EXPANDED -> DIMENSION_EXPANDED
    +            DIMENSION_TYPE_HINGE -> DIMENSION_HINGE
    +            DIMENSION_TYPE_RATIO ->
    +                Dimension.ratio(dimensionBundle.getFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))
    +            DIMENSION_TYPE_PIXEL ->
    +                Dimension.pixel(dimensionBundle.getInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE))
    +            else -> throw IllegalArgumentException("Illegal type $type")
    +        }
    +    }
    +
    +    /**
    +     * Puts [EmbeddingBounds.Dimension] information into bundle with a given [key].
    +     *
    +     * [EmbeddingBounds.Dimension] is encoded as a [Bundle] with following data structure:
    +     * - [KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE]: A `string` type. Must be one of:
    +     *     - [DIMENSION_TYPE_EXPANDED]
    +     *     - [DIMENSION_TYPE_HINGE]
    +     *     - [DIMENSION_TYPE_RATIO]
    +     *     - [DIMENSION_TYPE_PIXEL]
    +     * - [KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE]: Only specified for [DIMENSION_TYPE_RATIO] and
    +     * [DIMENSION_TYPE_PIXEL]. [DIMENSION_TYPE_RATIO] requires a [Float], while
    +     * [DIMENSION_TYPE_PIXEL] requires a [Int].
    +     */
    +    private fun Bundle.putDimension(key: String, dimension: Dimension) {
    +        putBundle(
    +            key,
    +            Bundle().apply {
    +                when (dimension) {
    +                    DIMENSION_EXPANDED -> {
    +                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_EXPANDED)
    +                    }
    +
    +                    DIMENSION_HINGE -> {
    +                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_HINGE)
    +                    }
    +
    +                    is Dimension.Ratio -> {
    +                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_RATIO)
    +                        putFloat(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)
    +                    }
    +
    +                    is Dimension.Pixel -> {
    +                        putString(KEY_EMBEDDING_BOUNDS_DIMENSION_TYPE, DIMENSION_TYPE_PIXEL)
    +                        putInt(KEY_EMBEDDING_BOUNDS_DIMENSION_VALUE, dimension.value)
    +                    }
    +                }
    +            }
    +        )
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    internal fun setActivityStackToken(options: Bundle, activityStackToken: Token) {
    +        options.putBundle(KEY_ACTIVITY_STACK_TOKEN, activityStackToken.toBundle())
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
    index 5a5df53..907e294 100644
    --- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
    
    @@ -17,13 +17,15 @@
     
     import android.app.Activity
     import androidx.annotation.RestrictTo
    -import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.extensions.embedding.ActivityStack.Token
     
     /**
      * A container that holds a stack of activities, overlapping and bound to the same rectangle on the
      * screen.
      */
    -class ActivityStack @RestrictTo(LIBRARY_GROUP) constructor(
    +class ActivityStack internal constructor(
         /**
          * The [Activity] list in this application's process that belongs to this [ActivityStack].
          *
    @@ -41,9 +43,32 @@
          * `false`.
          */
         val isEmpty: Boolean,
    +    /**
    +     * A token uniquely identifying this `ActivityStack`.
    +     */
    +    private val token: Token?,
     ) {
     
         /**
    +     * Creates ActivityStack ONLY for testing.
    +     *
    +     * @param activitiesInProcess the [Activity] list in this application's process that belongs to
    +     * this [ActivityStack].
    +     * @param isEmpty whether there is no [Activity] running in this [ActivityStack].
    +     */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    constructor(
    +        activitiesInProcess: List,
    +        isEmpty: Boolean
    +    ) : this(activitiesInProcess, isEmpty, token = null)
    +
    +    @RequiresWindowSdkExtension(5)
    +    internal fun getToken(): Token = let {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
    +        token!!
    +    }
    +
    +    /**
          * Whether this [ActivityStack] contains the [activity].
          */
         operator fun contains(activity: Activity): Boolean {
    @@ -56,6 +81,7 @@
     
             if (activitiesInProcess != other.activitiesInProcess) return false
             if (isEmpty != other.isEmpty) return false
    +        if (token != other.token) return false
     
             return true
         }
    @@ -63,6 +89,7 @@
         override fun hashCode(): Int {
             var result = activitiesInProcess.hashCode()
             result = 31 * result + isEmpty.hashCode()
    +        result = 31 * result + token.hashCode()
             return result
         }
     
    @@ -70,5 +97,6 @@
             "ActivityStack{" +
                 "activitiesInProcess=$activitiesInProcess" +
                 ", isEmpty=$isEmpty" +
    +            ", token=$token" +
                 "}"
     }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt b/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt
    new file mode 100644
    index 0000000..08db240
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/ActivityWindowInfoCallbackController.kt
    
    @@ -0,0 +1,141 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.app.Activity
    +import android.graphics.Rect
    +import android.util.ArrayMap
    +import androidx.annotation.GuardedBy
    +import androidx.annotation.VisibleForTesting
    +import androidx.core.util.Consumer as JetpackConsumer
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.extensions.core.util.function.Consumer
    +import androidx.window.extensions.embedding.ActivityEmbeddingComponent
    +import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo as ExtensionsActivityWindowInfo
    +import java.util.concurrent.locks.ReentrantLock
    +import kotlin.concurrent.withLock
    +
    +/** Manages and dispatches update of [EmbeddedActivityWindowInfo]. */
    +@RequiresWindowSdkExtension(6)
    +internal open class ActivityWindowInfoCallbackController(
    +    private val embeddingExtension: ActivityEmbeddingComponent,
    +) {
    +    private val globalLock = ReentrantLock()
    +
    +    @GuardedBy("globalLock")
    +    private val extensionsCallback: Consumer
    +
    +    @VisibleForTesting
    +    @GuardedBy("globalLock")
    +    internal var callbacks:
    +        MutableMap, CallbackWrapper> = ArrayMap()
    +
    +    init {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
    +        extensionsCallback = Consumer { info ->
    +            globalLock.withLock {
    +                for (callbackWrapper in callbacks.values) {
    +                    callbackWrapper.accept(info)
    +                }
    +            }
    +        }
    +    }
    +
    +    fun addCallback(
    +        activity: Activity,
    +        callback: JetpackConsumer
    +    ) {
    +        globalLock.withLock {
    +            if (callbacks.isEmpty()) {
    +                // Register when the first callback is added.
    +                embeddingExtension.setEmbeddedActivityWindowInfoCallback(
    +                    Runnable::run,
    +                    extensionsCallback
    +                )
    +            }
    +
    +            val callbackWrapper = CallbackWrapper(activity, callback)
    +            callbacks[callback] = callbackWrapper
    +            embeddingExtension.getEmbeddedActivityWindowInfo(activity)?.apply {
    +                // Trigger with the latest info if the window exists.
    +                callbackWrapper.accept(this)
    +            }
    +        }
    +    }
    +
    +    fun removeCallback(callback: JetpackConsumer) {
    +        globalLock.withLock {
    +            if (callbacks.remove(callback) == null) {
    +                // Early return if the callback is not registered.
    +                return
    +            }
    +            if (callbacks.isEmpty()) {
    +                // Unregister when the last callback is removed.
    +                embeddingExtension.clearEmbeddedActivityWindowInfoCallback()
    +            }
    +        }
    +    }
    +
    +    /** Translates from Extensions info to Jetpack info. */
    +    @VisibleForTesting
    +    internal open fun translate(info: ExtensionsActivityWindowInfo): EmbeddedActivityWindowInfo {
    +        val parentHostBounds = Rect(info.taskBounds)
    +        val boundsInParentHost = Rect(info.activityStackBounds)
    +        // Converting to host container coordinate.
    +        boundsInParentHost.offset(-parentHostBounds.left, - parentHostBounds.top)
    +        return EmbeddedActivityWindowInfo(
    +            isEmbedded = info.isEmbedded,
    +            parentHostBounds = parentHostBounds,
    +            boundsInParentHost = boundsInParentHost
    +        )
    +    }
    +
    +    @VisibleForTesting
    +    internal inner class CallbackWrapper(
    +        private val activity: Activity,
    +        val callback: JetpackConsumer
    +    ) {
    +        var lastReportedInfo: EmbeddedActivityWindowInfo? = null
    +
    +        fun accept(extensionsActivityWindowInfo: ExtensionsActivityWindowInfo) {
    +            val updatedActivity = extensionsActivityWindowInfo.activity
    +            if (activity != updatedActivity) {
    +                return
    +            }
    +
    +            val newInfo = translate(extensionsActivityWindowInfo)
    +            if (shouldReportInfo(newInfo)) {
    +                lastReportedInfo = newInfo
    +                callback.accept(newInfo)
    +            }
    +        }
    +
    +        private fun shouldReportInfo(newInfo: EmbeddedActivityWindowInfo): Boolean =
    +            lastReportedInfo?.let {
    +                if (it.isEmbedded != newInfo.isEmbedded) {
    +                    // Always report if the embedded status changes
    +                    return true
    +                }
    +                if (!newInfo.isEmbedded) {
    +                    // Do not report if the activity is not embedded
    +                    return false
    +                }
    +                return it != newInfo
    +            } ?: newInfo.isEmbedded // Always report the first available info if it is embedded
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
    new file mode 100644
    index 0000000..f67a295
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/DividerAttributes.kt
    
    @@ -0,0 +1,340 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.graphics.Color
    +import androidx.annotation.ColorInt
    +import androidx.annotation.FloatRange
    +import androidx.annotation.IntRange
    +import androidx.window.RequiresWindowSdkExtension
    +
    +/**
    + * The attributes of the divider layout and behavior.
    + *
    + * @property widthDp                the width of the divider.
    + * @property color                  the color of the divider.
    + *
    + * @see SplitAttributes.Builder.setDividerAttributes
    + * @see FixedDividerAttributes
    + * @see DraggableDividerAttributes
    + * @see NO_DIVIDER
    + */
    +abstract class DividerAttributes private constructor(
    +    @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) val widthDp: Int = WIDTH_SYSTEM_DEFAULT,
    +    @ColorInt val color: Int = Color.BLACK,
    +) {
    +    override fun toString(): String = DividerAttributes::class.java.simpleName + "{" +
    +        "width=$widthDp, " +
    +        "color=$color" +
    +        "}"
    +
    +    /**
    +     * The attributes of a fixed divider. A fixed divider is a divider type that draws a static line
    +     * between the primary and secondary containers.
    +     *
    +     * @property widthDp                the width of the divider.
    +     * @property color                  the color of the divider.
    +     *
    +     * @see SplitAttributes.Builder.setDividerAttributes
    +     */
    +    class FixedDividerAttributes @RequiresWindowSdkExtension(6) private constructor(
    +        @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
    +        @ColorInt color: Int = Color.BLACK
    +    ) : DividerAttributes(widthDp, color) {
    +
    +        override fun equals(other: Any?): Boolean {
    +            if (this === other) return true
    +            if (other !is FixedDividerAttributes) return false
    +            return widthDp == other.widthDp && color == other.color
    +        }
    +
    +        override fun hashCode(): Int = widthDp * 31 + color
    +
    +        /**
    +         * The [FixedDividerAttributes] builder.
    +         *
    +         * @constructor creates a new [FixedDividerAttributes.Builder]
    +         */
    +        @RequiresWindowSdkExtension(6)
    +        class Builder() {
    +            @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
    +            private var widthDp = WIDTH_SYSTEM_DEFAULT
    +
    +            @ColorInt
    +            private var color = Color.BLACK
    +
    +            /**
    +             * The [FixedDividerAttributes] builder constructor initialized by an existing
    +             * [FixedDividerAttributes].
    +             *
    +             * @param original the original [FixedDividerAttributes] to initialize the [Builder].
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            constructor(original: FixedDividerAttributes) : this() {
    +                widthDp = original.widthDp
    +                color = original.color
    +            }
    +
    +            /**
    +             * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the
    +             * system will choose a default value based on the display size and form factor.
    +             *
    +             * @throws IllegalArgumentException if the provided value is invalid.
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =
    +                apply {
    +                    validateWidth(widthDp)
    +                    this.widthDp = widthDp
    +                }
    +
    +            /**
    +             * Sets the color of the divider. If not set, the default color [Color.BLACK] is
    +             * used.
    +             *
    +             * @throws IllegalArgumentException if the provided value is invalid.
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            fun setColor(@ColorInt color: Int): Builder =
    +                apply {
    +                    validateColor(color)
    +                    this.color = color
    +                }
    +
    +            /** Builds a [FixedDividerAttributes] instance. */
    +            @RequiresWindowSdkExtension(6)
    +            fun build(): FixedDividerAttributes {
    +                return FixedDividerAttributes(widthDp = widthDp, color = color)
    +            }
    +        }
    +    }
    +
    +    /**
    +     * The attributes of a draggable divider. A draggable divider draws a line between the primary
    +     * and secondary containers with a drag handle that the user can drag and resize the containers.
    +     *
    +     * @property widthDp       the width of the divider.
    +     * @property color         the color of the divider.
    +     * @property dragRange     the range that a divider is allowed to be dragged. When the user
    +     * drags the divider beyond this range, the system will choose to either fully expand the
    +     * container or move the divider back into the range.
    +     *
    +     * @see SplitAttributes.Builder.setDividerAttributes
    +     */
    +    class DraggableDividerAttributes @RequiresWindowSdkExtension(6) private constructor(
    +        @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int = WIDTH_SYSTEM_DEFAULT,
    +        @ColorInt color: Int = Color.BLACK,
    +        val dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT,
    +    ) : DividerAttributes(widthDp, color) {
    +
    +        override fun equals(other: Any?): Boolean {
    +            if (this === other) return true
    +            if (other !is DraggableDividerAttributes) return false
    +            return widthDp == other.widthDp && color == other.color &&
    +                dragRange == other.dragRange
    +        }
    +
    +        override fun hashCode(): Int = (widthDp * 31 + color) * 31 + dragRange.hashCode()
    +
    +        override fun toString(): String = DividerAttributes::class.java.simpleName + "{" +
    +            "width=$widthDp, " +
    +            "color=$color, " +
    +            "primaryContainerDragRange=$dragRange" +
    +            "}"
    +
    +        /**
    +         * The [DraggableDividerAttributes] builder.
    +         *
    +         * @constructor creates a new [DraggableDividerAttributes.Builder]
    +         */
    +        @RequiresWindowSdkExtension(6)
    +        class Builder() {
    +            @IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong())
    +            private var widthDp = WIDTH_SYSTEM_DEFAULT
    +
    +            @ColorInt
    +            private var color = Color.BLACK
    +
    +            private var dragRange: DragRange = DragRange.DRAG_RANGE_SYSTEM_DEFAULT
    +
    +            /**
    +             * The [DraggableDividerAttributes] builder constructor initialized by an existing
    +             * [DraggableDividerAttributes].
    +             *
    +             * @param original the original [DraggableDividerAttributes] to initialize the [Builder]
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            constructor(original: DraggableDividerAttributes) : this() {
    +                widthDp = original.widthDp
    +                dragRange = original.dragRange
    +                color = original.color
    +            }
    +
    +            /**
    +             * Sets the divider width. It defaults to [WIDTH_SYSTEM_DEFAULT], which means the
    +             * system will choose a default value based on the display size and form factor.
    +             *
    +             * @throws IllegalArgumentException if the provided value is invalid.
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            fun setWidthDp(@IntRange(from = WIDTH_SYSTEM_DEFAULT.toLong()) widthDp: Int): Builder =
    +                apply {
    +                    validateWidth(widthDp)
    +                    this.widthDp = widthDp
    +                }
    +
    +            /**
    +             * Sets the color of the divider. If not set, the default color [Color.BLACK] is
    +             * used.
    +             *
    +             * @throws IllegalArgumentException if the provided value is invalid.
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            fun setColor(@ColorInt color: Int): Builder =
    +                apply {
    +                    validateColor(color)
    +                    this.color = color
    +                }
    +
    +            /**
    +             * Sets the drag range of the divider in terms of the split ratio of the primary
    +             * container. It defaults to [DragRange.DRAG_RANGE_SYSTEM_DEFAULT], which means the
    +             * system will choose a default value based on the display size and form factor.
    +             *
    +             * When the user drags the divider beyond this range, the system will choose to either
    +             * fully expand the container or move the divider back into the range.
    +             *
    +             * @param dragRange the [DragRange] for the draggable divider.
    +             */
    +            @RequiresWindowSdkExtension(6)
    +            fun setDragRange(dragRange: DragRange): Builder =
    +                apply { this.dragRange = dragRange }
    +
    +            /** Builds a [DividerAttributes] instance. */
    +            @RequiresWindowSdkExtension(6)
    +            fun build(): DraggableDividerAttributes =
    +                DraggableDividerAttributes(
    +                    widthDp = widthDp,
    +                    color = color,
    +                    dragRange = dragRange,
    +                )
    +        }
    +    }
    +
    +    /**
    +     * Describes the range that the user is allowed to drag the draggable divider.
    +     *
    +     * @see SplitRatioDragRange
    +     * @see DRAG_RANGE_SYSTEM_DEFAULT
    +     */
    +    abstract class DragRange private constructor() {
    +        /**
    +         * A drag range represented as an interval of the primary container's split ratios.
    +         *
    +         * @property minRatio the minimum split ratio of the primary container that the user is
    +         * allowed to drag to. When the divider is dragged beyond this ratio, the system will choose
    +         * to either fully expand the secondary container, or move the divider back to this ratio.
    +         * @property maxRatio the maximum split ratio of the primary container that the user is
    +         * allowed to drag to. When the divider is dragged beyond this ratio, the system will choose
    +         * to either fully expand the primary container, or move the divider back to this ratio.
    +         *
    +         * @constructor constructs a new [SplitRatioDragRange]
    +         * @throws IllegalArgumentException if the provided values are invalid.
    +         */
    +        class SplitRatioDragRange(
    +            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
    +            val minRatio: Float,
    +            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
    +            val maxRatio: Float,
    +        ) : DragRange() {
    +            init {
    +                if (minRatio <= 0.0 || minRatio >= 1.0) {
    +                    throw IllegalArgumentException(
    +                        "minRatio must be in the interval (0.0, 1.0)"
    +                    )
    +                }
    +                if (maxRatio <= 0.0 || maxRatio >= 1.0) {
    +                    throw IllegalArgumentException(
    +                        "maxRatio must be in the interval (0.0, 1.0)"
    +                    )
    +                }
    +                if (minRatio > maxRatio) {
    +                    throw IllegalArgumentException(
    +                        "minRatio must be less than or equal to maxRatio"
    +                    )
    +                }
    +            }
    +
    +            override fun toString(): String = "SplitRatioDragRange[$minRatio, $maxRatio]"
    +
    +            override fun equals(other: Any?): Boolean {
    +                if (this === other) return true
    +                if (other !is SplitRatioDragRange) return false
    +                return minRatio == other.minRatio && maxRatio == other.maxRatio
    +            }
    +
    +            override fun hashCode(): Int = minRatio.hashCode() * 31 + maxRatio.hashCode()
    +        }
    +
    +        companion object {
    +            /**
    +             * A special value to indicate that the system will choose default values based on the
    +             * display size and form factor.
    +             *
    +             * @see DraggableDividerAttributes.dragRange
    +             */
    +            @JvmField
    +            val DRAG_RANGE_SYSTEM_DEFAULT = object : DragRange() {
    +                override fun toString(): String = "DRAG_RANGE_SYSTEM_DEFAULT"
    +            }
    +        }
    +    }
    +
    +    companion object {
    +        /**
    +         * A special value to indicate that the system will choose a default value based on the
    +         * display size and form factor.
    +         *
    +         * @see DividerAttributes.widthDp
    +         */
    +        const val WIDTH_SYSTEM_DEFAULT: Int = -1
    +
    +        /** Indicates that no divider is requested. */
    +        @JvmField
    +        val NO_DIVIDER = object : DividerAttributes() {
    +            override fun toString(): String = "NO_DIVIDER"
    +        }
    +
    +        private fun validateWidth(widthDp: Int) = run {
    +            require(widthDp == WIDTH_SYSTEM_DEFAULT || widthDp >= 0) {
    +                "widthDp must be greater than or equal to 0 or WIDTH_SYSTEM_DEFAULT. Got: $widthDp"
    +            }
    +        }
    +
    +        private fun validateColor(@ColorInt color: Int) = run {
    +            require(color.alpha() == 255) {
    +                "Divider color must be opaque. Got: ${Integer.toHexString(color)}"
    +            }
    +        }
    +
    +        /**
    +         * Returns the alpha value of the color. This is the same as [Color.alpha] and is used to
    +         * avoid test-time dependency.
    +         */
    +        private fun Int.alpha() = this ushr 24
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt
    new file mode 100644
    index 0000000..f687555
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddedActivityWindowInfo.kt
    
    @@ -0,0 +1,73 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.graphics.Rect
    +
    +/**
    + * Describes the embedded window related info of an activity.
    + *
    + * When the activity is embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo] will
    + * be invoked when any fields of [EmbeddedActivityWindowInfo] is changed.
    + * When the activity is not embedded, the [ActivityEmbeddingController.embeddedActivityWindowInfo]
    + * will not be triggered unless the activity is becoming non-embedded from embedded, in which case
    + * [isEmbedded] will be `false`.
    + *
    + * @see ActivityEmbeddingController.embeddedActivityWindowInfo
    + */
    +class EmbeddedActivityWindowInfo internal constructor(
    +    /**
    +     * Whether this activity is embedded and its presentation may be customized by the host
    +     * process of the task it is associated with.
    +     */
    +    val isEmbedded: Boolean,
    +    /**
    +     * The bounds of the host container in display coordinate space, which should be the Task bounds
    +     * for regular embedding use case, or if the activity is not embedded.
    +     */
    +    val parentHostBounds: Rect,
    +    /**
    +     * The relative bounds of the embedded [ActivityStack] in the host container coordinate space.
    +     * It has the same size as [parentHostBounds] if the activity is not embedded.
    +     */
    +    val boundsInParentHost: Rect,
    +) {
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) return true
    +        if (other !is EmbeddedActivityWindowInfo) return false
    +
    +        if (isEmbedded != other.isEmbedded) return false
    +        if (parentHostBounds != other.parentHostBounds) return false
    +        if (boundsInParentHost != other.boundsInParentHost) return false
    +
    +        return true
    +    }
    +
    +    override fun hashCode(): Int {
    +        var result = isEmbedded.hashCode()
    +        result = 31 * result + parentHostBounds.hashCode()
    +        result = 31 * result + boundsInParentHost.hashCode()
    +        return result
    +    }
    +
    +    override fun toString(): String =
    +        "EmbeddedActivityWindowInfo{" +
    +            "isEmbedded=$isEmbedded" +
    +            ", parentHostBounds=$parentHostBounds" +
    +            ", boundsInParentHost=$boundsInParentHost" +
    +            "}"
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
    index 1b40811..35b3e1a 100644
    --- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
    
    @@ -22,10 +22,19 @@
     import android.content.Intent
     import android.os.Binder
     import android.util.LayoutDirection
    +import android.util.Log
     import android.util.Pair as AndroidPair
    -import android.view.WindowMetrics
    +import android.view.WindowMetrics as AndroidWindowMetrics
    +import androidx.window.RequiresWindowSdkExtension
     import androidx.window.WindowSdkExtensions
    +import androidx.window.core.Bounds
    +import androidx.window.core.ExperimentalWindowApi
     import androidx.window.core.PredicateAdapter
    +import androidx.window.embedding.DividerAttributes.DragRange.Companion.DRAG_RANGE_SYSTEM_DEFAULT
    +import androidx.window.embedding.DividerAttributes.DragRange.SplitRatioDragRange
    +import androidx.window.embedding.DividerAttributes.DraggableDividerAttributes
    +import androidx.window.embedding.DividerAttributes.FixedDividerAttributes
    +import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior.Companion.ON_ACTIVITY_STACK
     import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.BOTTOM_TO_TOP
     import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LEFT_TO_RIGHT
     import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
    @@ -40,7 +49,12 @@
     import androidx.window.extensions.core.util.function.Predicate
     import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
     import androidx.window.extensions.embedding.ActivityRule.Builder as ActivityRuleBuilder
    +import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
    +import androidx.window.extensions.embedding.AnimationBackground as OEMEmbeddingAnimationBackground
    +import androidx.window.extensions.embedding.DividerAttributes as OEMDividerAttributes
    +import androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT
     import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
    +import androidx.window.extensions.embedding.ParentContainerInfo as OEMParentContainerInfo
     import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
     import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
     import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType
    @@ -51,10 +65,15 @@
     import androidx.window.extensions.embedding.SplitPairRule.FINISH_ADJACENT
     import androidx.window.extensions.embedding.SplitPairRule.FINISH_ALWAYS
     import androidx.window.extensions.embedding.SplitPairRule.FINISH_NEVER
    +import androidx.window.extensions.embedding.SplitPinRule as OEMSplitPinRule
    +import androidx.window.extensions.embedding.SplitPinRule.Builder as SplitPinRuleBuilder
     import androidx.window.extensions.embedding.SplitPlaceholderRule as OEMSplitPlaceholderRule
     import androidx.window.extensions.embedding.SplitPlaceholderRule.Builder as SplitPlaceholderRuleBuilder
    +import androidx.window.extensions.embedding.WindowAttributes
    +import androidx.window.extensions.embedding.WindowAttributes as OEMWindowAttributes
     import androidx.window.layout.WindowMetricsCalculator
     import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
    +import androidx.window.layout.util.DensityCompatHelper
     
     /**
      * Adapter class that translates data classes between Extension and Jetpack interfaces.
    @@ -62,40 +81,47 @@
     internal class EmbeddingAdapter(
         private val predicateAdapter: PredicateAdapter
     ) {
    -    private val vendorApiLevel
    +    private val extensionVersion
             get() = WindowSdkExtensions.getInstance().extensionVersion
         private val api1Impl = VendorApiLevel1Impl(predicateAdapter)
         private val api2Impl = VendorApiLevel2Impl()
    +    private val api3Impl = VendorApiLevel3Impl()
    +    @OptIn(ExperimentalWindowApi::class)
    +    var embeddingConfiguration: EmbeddingConfiguration? = null
     
         fun translate(splitInfoList: List): List {
             return splitInfoList.map(this::translate)
         }
     
         private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
    -        return when (vendorApiLevel) {
    +        return when (extensionVersion) {
                 1 -> api1Impl.translateCompat(splitInfo)
                 2 -> api2Impl.translateCompat(splitInfo)
    -            else -> {
    -                val primaryActivityStack = splitInfo.primaryActivityStack
    -                val secondaryActivityStack = splitInfo.secondaryActivityStack
    -                SplitInfo(
    -                    ActivityStack(
    -                        primaryActivityStack.activities,
    -                        primaryActivityStack.isEmpty,
    -                    ),
    -                    ActivityStack(
    -                        secondaryActivityStack.activities,
    -                        secondaryActivityStack.isEmpty,
    -                    ),
    -                    translate(splitInfo.splitAttributes),
    -                    splitInfo.token,
    -                )
    -            }
    +            in 3..4 -> api3Impl.translateCompat(splitInfo)
    +            else -> SplitInfo(
    +                translate(splitInfo.primaryActivityStack),
    +                translate(splitInfo.secondaryActivityStack),
    +                translate(splitInfo.splitAttributes),
    +                splitInfo.splitInfoToken,
    +            )
             }
         }
     
    -    internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes =
    -        SplitAttributes.Builder()
    +    internal fun translate(activityStack: OEMActivityStack): ActivityStack =
    +        when (extensionVersion) {
    +            in 1..4 -> api1Impl.translateCompat(activityStack)
    +            else -> ActivityStack(
    +                activityStack.activities,
    +                activityStack.isEmpty,
    +                activityStack.activityStackToken,
    +            )
    +        }
    +
    +    internal fun translate(activityStacks: List): List =
    +        activityStacks.map(this::translate)
    +
    +    internal fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes {
    +        val builder = SplitAttributes.Builder()
                 .setSplitType(
                     when (val splitType = splitAttributes.splitType) {
                         is OEMSplitType.HingeSplitType -> SPLIT_TYPE_HINGE
    @@ -115,7 +141,46 @@
                         )
                     }
                 )
    -            .build()
    +        if (extensionVersion >= 5) {
    +            val animationBackground = splitAttributes.animationBackground
    +            builder.setAnimationBackground(
    +                if (animationBackground is OEMEmbeddingAnimationBackground.ColorBackground) {
    +                    EmbeddingAnimationBackground.createColorBackground(animationBackground.color)
    +                } else {
    +                    EmbeddingAnimationBackground.DEFAULT
    +                }
    +            )
    +        }
    +        if (extensionVersion >= 6) {
    +            builder.setDividerAttributes(
    +                translateDividerAttributes(splitAttributes.dividerAttributes))
    +        }
    +        return builder.build()
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    @OptIn(ExperimentalWindowApi::class)
    +    @SuppressLint("NewApi", "ClassVerificationFailure")
    +    internal fun translate(
    +        parentContainerInfo: OEMParentContainerInfo,
    +    ): ParentContainerInfo {
    +        val configuration = parentContainerInfo.configuration
    +        val density = DensityCompatHelper.getInstance()
    +            .density(parentContainerInfo.configuration, parentContainerInfo.windowMetrics)
    +        val windowMetrics = WindowMetricsCalculator
    +            .translateWindowMetrics(parentContainerInfo.windowMetrics, density)
    +
    +        return ParentContainerInfo(
    +            Bounds(windowMetrics.bounds),
    +            ExtensionsWindowLayoutInfoAdapter.translate(
    +                windowMetrics,
    +                parentContainerInfo.windowLayoutInfo
    +            ),
    +            windowMetrics.getWindowInsets(),
    +            configuration,
    +            density
    +        )
    +    }
     
         fun translateSplitAttributesCalculator(
             calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
    @@ -125,7 +190,7 @@
     
         @SuppressLint("NewApi")
         fun translate(
    -        params: OEMSplitAttributesCalculatorParams
    +        params: OEMSplitAttributesCalculatorParams,
         ): SplitAttributesCalculatorParams = let {
             val taskWindowMetrics = params.parentWindowMetrics
             val taskConfiguration = params.parentConfiguration
    @@ -133,8 +198,10 @@
             val defaultSplitAttributes = params.defaultSplitAttributes
             val areDefaultConstraintsSatisfied = params.areDefaultConstraintsSatisfied()
             val splitRuleTag = params.splitRuleTag
    -        val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)
    -
    +        val density = DensityCompatHelper.getInstance()
    +            .density(taskConfiguration, taskWindowMetrics)
    +        val windowMetrics = WindowMetricsCalculator
    +            .translateWindowMetrics(taskWindowMetrics, density)
             SplitAttributesCalculatorParams(
                 windowMetrics,
                 taskConfiguration,
    @@ -150,7 +217,7 @@
             rule: SplitPairRule,
             predicateClass: Class<*>
         ): OEMSplitPairRule {
    -        if (vendorApiLevel < 2) {
    +        if (extensionVersion < 2) {
                 return api1Impl.translateSplitPairRuleCompat(context, rule, predicateClass)
             } else {
                 val activitiesPairPredicate =
    @@ -168,7 +235,7 @@
                             )
                         }
                     }
    -            val windowMetricsPredicate = Predicate { windowMetrics ->
    +            val windowMetricsPredicate = Predicate { windowMetrics ->
                     rule.checkParentMetrics(context, windowMetrics)
                 }
                 val tag = rule.tag
    @@ -191,11 +258,30 @@
             }
         }
     
    +    @OptIn(ExperimentalWindowApi::class)
    +    fun translateSplitPinRule(context: Context, splitPinRule: SplitPinRule): OEMSplitPinRule {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
    +        val windowMetricsPredicate = Predicate { windowMetrics ->
    +            splitPinRule.checkParentMetrics(context, windowMetrics)
    +        }
    +        val builder = SplitPinRuleBuilder(
    +            translateSplitAttributes(splitPinRule.defaultSplitAttributes),
    +            windowMetricsPredicate
    +        )
    +        builder.setSticky(splitPinRule.isSticky)
    +        val tag = splitPinRule.tag
    +        if (tag != null) {
    +            builder.setTag(tag)
    +        }
    +        return builder.build()
    +    }
    +
    +    @OptIn(ExperimentalWindowApi::class)
         fun translateSplitAttributes(splitAttributes: SplitAttributes): OEMSplitAttributes {
    -        require(vendorApiLevel >= 2)
    +        require(extensionVersion >= 2)
             // To workaround the "unused" error in ktlint. It is necessary to translate SplitAttributes
             // from WM Jetpack version to WM extension version.
    -        return androidx.window.extensions.embedding.SplitAttributes.Builder()
    +        val builder = OEMSplitAttributes.Builder()
                 .setSplitType(translateSplitType(splitAttributes.splitType))
                 .setLayoutDirection(
                     when (splitAttributes.layoutDirection) {
    @@ -209,11 +295,33 @@
                         )
                     }
                 )
    -            .build()
    +        if (extensionVersion >= 5) {
    +            builder.setWindowAttributes(translateWindowAttributes())
    +                .setAnimationBackground(
    +                    translateAnimationBackground(splitAttributes.animationBackground))
    +        }
    +        if (extensionVersion >= 6) {
    +            builder.setDividerAttributes(
    +                translateDividerAttributes(splitAttributes.dividerAttributes))
    +        }
    +        return builder.build()
    +    }
    +
    +    /** Translates [embeddingConfiguration] from adapter to [WindowAttributes]. */
    +    @OptIn(ExperimentalWindowApi::class)
    +    internal fun translateWindowAttributes(): OEMWindowAttributes = let {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
    +
    +        OEMWindowAttributes(
    +            when (embeddingConfiguration?.dimAreaBehavior) {
    +                ON_ACTIVITY_STACK -> OEMWindowAttributes.DIM_AREA_ON_ACTIVITY_STACK
    +                else -> OEMWindowAttributes.DIM_AREA_ON_TASK
    +            }
    +        )
         }
     
         private fun translateSplitType(splitType: SplitType): OEMSplitType {
    -        require(vendorApiLevel >= 2)
    +        require(extensionVersion >= 2)
             return when (splitType) {
                 SPLIT_TYPE_HINGE -> OEMSplitType.HingeSplitType(
                     translateSplitType(SPLIT_TYPE_EQUAL)
    @@ -236,7 +344,7 @@
             rule: SplitPlaceholderRule,
             predicateClass: Class<*>
         ): OEMSplitPlaceholderRule {
    -        if (vendorApiLevel < 2) {
    +        if (extensionVersion < 2) {
                 return api1Impl.translateSplitPlaceholderRuleCompat(
                     context,
                     rule,
    @@ -249,7 +357,7 @@
                 val intentPredicate = Predicate { intent ->
                     rule.filters.any { filter -> filter.matchesIntent(intent) }
                 }
    -            val windowMetricsPredicate = Predicate { windowMetrics ->
    +            val windowMetricsPredicate = Predicate { windowMetrics ->
                     rule.checkParentMetrics(context, windowMetrics)
                 }
                 val tag = rule.tag
    @@ -283,7 +391,7 @@
             rule: ActivityRule,
             predicateClass: Class<*>
         ): OEMActivityRule {
    -        if (vendorApiLevel < 2) {
    +        if (extensionVersion < 2) {
                 return api1Impl.translateActivityRuleCompat(rule, predicateClass)
             } else {
                 val activityPredicate = Predicate { activity ->
    @@ -315,27 +423,106 @@
             }.toSet()
         }
     
    +    private fun translateAnimationBackground(
    +        animationBackground: EmbeddingAnimationBackground
    +    ): OEMEmbeddingAnimationBackground {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
    +        return if (animationBackground is EmbeddingAnimationBackground.ColorBackground) {
    +            OEMEmbeddingAnimationBackground.createColorBackground(animationBackground.color)
    +        } else {
    +            OEMEmbeddingAnimationBackground.ANIMATION_BACKGROUND_DEFAULT
    +        }
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun translateDividerAttributes(
    +        dividerAttributes: DividerAttributes
    +    ): OEMDividerAttributes? {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
    +        if (dividerAttributes === DividerAttributes.NO_DIVIDER) {
    +            return null
    +        }
    +        val builder = OEMDividerAttributes.Builder(
    +            when (dividerAttributes) {
    +                is FixedDividerAttributes -> OEMDividerAttributes.DIVIDER_TYPE_FIXED
    +                is DraggableDividerAttributes -> OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE
    +                else ->
    +                    throw IllegalArgumentException("Unknown divider attributes $dividerAttributes")
    +            }
    +        )
    +            .setDividerColor(dividerAttributes.color)
    +            .setWidthDp(dividerAttributes.widthDp)
    +
    +        if (dividerAttributes is DraggableDividerAttributes &&
    +            dividerAttributes.dragRange is SplitRatioDragRange
    +        ) {
    +            builder
    +                .setPrimaryMinRatio(dividerAttributes.dragRange.minRatio)
    +                .setPrimaryMaxRatio(dividerAttributes.dragRange.maxRatio)
    +        }
    +        return builder.build()
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun translateDividerAttributes(
    +        oemDividerAttributes: OEMDividerAttributes?
    +    ): DividerAttributes {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
    +        if (oemDividerAttributes == null) {
    +            return DividerAttributes.NO_DIVIDER
    +        }
    +        return when (oemDividerAttributes.dividerType) {
    +            OEMDividerAttributes.DIVIDER_TYPE_FIXED ->
    +                FixedDividerAttributes.Builder()
    +                    .setWidthDp(oemDividerAttributes.widthDp)
    +                    .setColor(oemDividerAttributes.dividerColor)
    +                    .build()
    +            OEMDividerAttributes.DIVIDER_TYPE_DRAGGABLE ->
    +                DraggableDividerAttributes.Builder()
    +                    .setWidthDp(oemDividerAttributes.widthDp)
    +                    .setColor(oemDividerAttributes.dividerColor)
    +                    .setDragRange(
    +                        if (oemDividerAttributes.primaryMinRatio == RATIO_SYSTEM_DEFAULT &&
    +                            oemDividerAttributes.primaryMaxRatio == RATIO_SYSTEM_DEFAULT
    +                        )
    +                            DRAG_RANGE_SYSTEM_DEFAULT
    +                        else
    +                            SplitRatioDragRange(
    +                                oemDividerAttributes.primaryMinRatio,
    +                                oemDividerAttributes.primaryMaxRatio,
    +                            )
    +                    ).build()
    +            // Default to DividerType.FIXED
    +            else -> {
    +                Log.w(TAG, "Unknown divider type $oemDividerAttributes.dividerType, default" +
    +                    " to fixed divider type")
    +                FixedDividerAttributes.Builder()
    +                    .setWidthDp(oemDividerAttributes.widthDp)
    +                    .setColor(oemDividerAttributes.dividerColor)
    +                    .build()
    +            }
    +        }
    +    }
    +
    +    /** Provides backward compatibility for Window extensions with API level 3 */
    +    // Suppress deprecation because this object is to provide backward compatibility.
    +    @Suppress("DEPRECATION")
    +    private inner class VendorApiLevel3Impl {
    +        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo = SplitInfo(
    +            api1Impl.translateCompat(splitInfo.primaryActivityStack),
    +            api1Impl.translateCompat(splitInfo.secondaryActivityStack),
    +            translate(splitInfo.splitAttributes),
    +            splitInfo.token,
    +        )
    +    }
    +
         /** Provides backward compatibility for Window extensions with API level 2 */
         private inner class VendorApiLevel2Impl {
    -        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo {
    -            val primaryActivityStack = splitInfo.primaryActivityStack
    -            val primaryFragment = ActivityStack(
    -                primaryActivityStack.activities,
    -                primaryActivityStack.isEmpty,
    -            )
    -
    -            val secondaryActivityStack = splitInfo.secondaryActivityStack
    -            val secondaryFragment = ActivityStack(
    -                secondaryActivityStack.activities,
    -                secondaryActivityStack.isEmpty,
    -            )
    -            return SplitInfo(
    -                primaryFragment,
    -                secondaryFragment,
    +        fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo = SplitInfo(
    +                api1Impl.translateCompat(splitInfo.primaryActivityStack),
    +                api1Impl.translateCompat(splitInfo.secondaryActivityStack),
                     translate(splitInfo.splitAttributes),
    -                INVALID_SPLIT_INFO_TOKEN,
                 )
    -        }
         }
     
         /**
    @@ -490,34 +677,29 @@
     
             @SuppressLint("ClassVerificationFailure", "NewApi")
             private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any =
    -            predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
    +            predicateAdapter.buildPredicate(AndroidWindowMetrics::class) { windowMetrics ->
                     splitRule.checkParentMetrics(context, windowMetrics)
                 }
     
             fun translateCompat(splitInfo: OEMSplitInfo): SplitInfo = SplitInfo(
    -                ActivityStack(
    -                    splitInfo.primaryActivityStack.activities,
    -                    splitInfo.primaryActivityStack.isEmpty,
    -                ),
    -                ActivityStack(
    -                    splitInfo.secondaryActivityStack.activities,
    -                    splitInfo.secondaryActivityStack.isEmpty,
    -                ),
    +                translateCompat(splitInfo.primaryActivityStack),
    +                translateCompat(splitInfo.secondaryActivityStack),
                     getSplitAttributesCompat(splitInfo),
    -                INVALID_SPLIT_INFO_TOKEN,
                 )
    +
    +        fun translateCompat(activityStack: OEMActivityStack): ActivityStack = ActivityStack(
    +            activityStack.activities,
    +            activityStack.isEmpty,
    +        )
         }
     
         internal companion object {
    +        private val TAG = EmbeddingAdapter::class.simpleName
    +
             /**
              * The default token of [SplitInfo], which provides compatibility for device prior to
              * vendor API level 3
              */
             val INVALID_SPLIT_INFO_TOKEN = Binder()
    -        /**
    -         * The default token of [ActivityStack], which provides compatibility for device prior to
    -         * vendor API level 3
    -         */
    -        val INVALID_ACTIVITY_STACK_TOKEN = Binder()
         }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt
    new file mode 100644
    index 0000000..d713f8b
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAnimationBackground.kt
    
    @@ -0,0 +1,113 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.graphics.Color
    +import androidx.annotation.ColorInt
    +import androidx.annotation.IntRange
    +
    +/**
    + * Background to be used for window transition animations for embedding activities if the animation
    + * requires a background.
    + *
    + * @see SplitAttributes.animationBackground
    + */
    +abstract class EmbeddingAnimationBackground private constructor() {
    +
    +    /**
    +     * An {@link EmbeddingAnimationBackground} to specify of using a developer-defined color as the
    +     * animation background.
    +     * Only opaque background is supported.
    +     *
    +     * @see EmbeddingAnimationBackground.createColorBackground
    +     */
    +    class ColorBackground internal constructor(
    +        /**
    +         * [ColorInt] to represent the color to use as the background color.
    +         */
    +        @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong())
    +        @ColorInt
    +        val color: Int
    +    ) : EmbeddingAnimationBackground() {
    +
    +        init {
    +            require(Color.alpha(color) == 255) {
    +                "Background color must be opaque"
    +            }
    +        }
    +
    +        override fun toString() = "ColorBackground{color:${Integer.toHexString(color)}}"
    +
    +        override fun equals(other: Any?): Boolean {
    +            if (other === this) return true
    +            if (other !is ColorBackground) return false
    +            return color == other.color
    +        }
    +
    +        override fun hashCode() = color.hashCode()
    +    }
    +
    +    /** @see EmbeddingAnimationBackground.DEFAULT */
    +    private class DefaultBackground : EmbeddingAnimationBackground() {
    +
    +        override fun toString() = "DefaultBackground"
    +    }
    +
    +    /**
    +     * Methods that create various [EmbeddingAnimationBackground].
    +     */
    +    companion object {
    +
    +        /**
    +         * Creates a [ColorBackground] to represent the given [color].
    +         *
    +         * Only opaque color is supported.
    +         *
    +         * @param color [ColorInt] of an opaque color.
    +         * @return the [ColorBackground] representing the [color].
    +         * @throws IllegalArgumentException if the [color] is not opaque.
    +         *
    +         * @see [DEFAULT] for the default value, which means to use the
    +         * current theme window background color.
    +         */
    +        @JvmStatic
    +        fun createColorBackground(
    +            @IntRange(from = Color.BLACK.toLong(), to = Color.WHITE.toLong())
    +            @ColorInt
    +            color: Int
    +        ): ColorBackground = ColorBackground(color)
    +
    +        /**
    +         * The special [EmbeddingAnimationBackground] to represent the default value,
    +         * which means to use the current theme window background color.
    +         */
    +        @JvmField
    +        val DEFAULT: EmbeddingAnimationBackground = DefaultBackground()
    +
    +        /**
    +         * Returns an [EmbeddingAnimationBackground] with the given [color]
    +         */
    +        internal fun buildFromValue(@ColorInt color: Int): EmbeddingAnimationBackground {
    +            return if (Color.alpha(color) != 255) {
    +                // Treat any non-opaque color as the default.
    +                DEFAULT
    +            } else {
    +                createColorBackground(color)
    +            }
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
    index cd74666..e0e865c 100644
    --- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
    
    @@ -17,9 +17,8 @@
     package androidx.window.embedding
     
     import android.app.Activity
    -import android.app.ActivityOptions
     import android.content.Context
    -import android.os.IBinder
    +import android.os.Bundle
     import androidx.annotation.RestrictTo
     import androidx.core.util.Consumer
     import androidx.window.RequiresWindowSdkExtension
    @@ -51,6 +50,12 @@
     
         fun isActivityEmbedded(activity: Activity): Boolean
     
    +    @RequiresWindowSdkExtension(5)
    +    fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun unpinTopActivityStack(taskId: Int)
    +
         @RequiresWindowSdkExtension(2)
         fun setSplitAttributesCalculator(
             calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
    @@ -61,15 +66,56 @@
     
         fun getActivityStack(activity: Activity): ActivityStack?
     
    -    @RequiresWindowSdkExtension(3)
    -    fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
    +    @RequiresWindowSdkExtension(5)
    +    fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun finishActivityStacks(activityStacks: Set)
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration)
     
         @RequiresWindowSdkExtension(3)
    -    fun invalidateTopVisibleSplitAttributes()
    +    fun invalidateVisibleActivityStacks()
     
         @RequiresWindowSdkExtension(3)
         fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
     
    +    @RequiresWindowSdkExtension(5)
    +    fun setOverlayAttributesCalculator(
    +        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
    +    )
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun clearOverlayAttributesCalculator()
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun addOverlayInfoCallback(
    +        overlayTag: String,
    +        executor: Executor,
    +        overlayInfoCallback: Consumer,
    +    )
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun removeOverlayInfoCallback(overlayInfoCallback: Consumer)
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun addEmbeddedActivityWindowInfoCallbackForActivity(
    +        activity: Activity,
    +        callback: Consumer
    +    )
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun removeEmbeddedActivityWindowInfoCallbackForActivity(
    +        callback: Consumer
    +    )
    +
         companion object {
     
             private var decorator: (EmbeddingBackend) -> EmbeddingBackend =
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
    new file mode 100644
    index 0000000..7f9f97c
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBounds.kt
    
    @@ -0,0 +1,465 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.graphics.Rect
    +import androidx.annotation.FloatRange
    +import androidx.annotation.IntRange
    +import androidx.annotation.Px
    +import androidx.annotation.VisibleForTesting
    +import androidx.window.core.Bounds
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_BOTTOM
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_LEFT
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_RIGHT
    +import androidx.window.embedding.EmbeddingBounds.Alignment.Companion.ALIGN_TOP
    +import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_EXPANDED
    +import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.DIMENSION_HINGE
    +import androidx.window.embedding.EmbeddingBounds.Dimension.Companion.ratio
    +import androidx.window.layout.FoldingFeature
    +import androidx.window.layout.WindowLayoutInfo
    +import kotlin.math.min
    +
    +/**
    + * The bounds of a standalone [ActivityStack].
    + *
    + * It can be either described with `alignment`, `width` and `height` or predefined constant values.
    + * Some important constants are:
    + * - [BOUNDS_EXPANDED]: To indicate the bounds fills the parent window container.
    + * - [BOUNDS_HINGE_TOP]: To indicate the bounds are at the top of the parent window container while
    + * its bottom follows the hinge position. Refer to [BOUNDS_HINGE_LEFT], [BOUNDS_HINGE_BOTTOM] and
    + * [BOUNDS_HINGE_RIGHT] for other bounds that follows the hinge position.
    + *
    + * @property alignment The alignment of the bounds relative to parent window container.
    + * @property width The width of the bounds.
    + * @property height The height of the bounds.
    + * @constructor creates an embedding bounds.
    + */
    +class EmbeddingBounds(val alignment: Alignment, val width: Dimension, val height: Dimension) {
    +    override fun toString(): String {
    +        return "Bounds:{alignment=$alignment, width=$width, height=$height}"
    +    }
    +
    +    override fun hashCode(): Int {
    +        var result = alignment.hashCode()
    +        result = result * 31 + width.hashCode()
    +        result = result * 31 + height.hashCode()
    +        return result
    +    }
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) return true
    +        if (other !is EmbeddingBounds) return false
    +        return alignment == other.alignment &&
    +            width == other.width &&
    +            height == other.height
    +    }
    +
    +    /** Returns `true` if the [width] should fallback to half of parent task width. */
    +    internal fun shouldUseFallbackDimensionForWidth(windowLayoutInfo: WindowLayoutInfo): Boolean {
    +        if (width != DIMENSION_HINGE) {
    +            return false
    +        }
    +        return !windowLayoutInfo.isVertical() ||
    +            alignment in listOf(ALIGN_TOP, ALIGN_BOTTOM)
    +    }
    +
    +    /** Returns `true` if the [height] should fallback to half of parent task height. */
    +    internal fun shouldUseFallbackDimensionForHeight(windowLayoutInfo: WindowLayoutInfo): Boolean {
    +        if (height != DIMENSION_HINGE) {
    +            return false
    +        }
    +        return !windowLayoutInfo.isHorizontal() ||
    +            alignment in listOf(ALIGN_LEFT, ALIGN_RIGHT)
    +    }
    +
    +    private fun WindowLayoutInfo.isHorizontal(): Boolean {
    +        val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false
    +        return foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
    +    }
    +
    +    private fun WindowLayoutInfo.isVertical(): Boolean {
    +        val foldingFeature = getOnlyFoldingFeatureOrNull() ?: return false
    +        return foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
    +    }
    +
    +    /**
    +     * Returns [FoldingFeature] if it's the only `FoldingFeature` in [WindowLayoutInfo]. Returns
    +     * `null`, otherwise.
    +     */
    +    private fun WindowLayoutInfo.getOnlyFoldingFeatureOrNull(): FoldingFeature? {
    +        val foldingFeatures = displayFeatures.filterIsInstance()
    +        return if (foldingFeatures.size == 1) foldingFeatures[0] else null
    +    }
    +
    +    /**
    +     * Calculates [width] in pixel with [parentContainerBounds] and [windowLayoutInfo].
    +     */
    +    @Px
    +    internal fun getWidthInPixel(
    +        parentContainerBounds: Bounds,
    +        windowLayoutInfo: WindowLayoutInfo
    +    ): Int {
    +        val taskWidth = parentContainerBounds.width
    +        val widthDimension = if (shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {
    +            ratio(0.5f)
    +        } else {
    +            width
    +        }
    +        when (widthDimension) {
    +            is Dimension.Ratio -> return widthDimension * taskWidth
    +            is Dimension.Pixel -> return min(taskWidth, widthDimension.value)
    +            DIMENSION_HINGE -> {
    +                // Should be verified by #shouldUseFallbackDimensionForWidth
    +                val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds
    +                return when (alignment) {
    +                    ALIGN_LEFT -> {
    +                        hingeBounds.left - parentContainerBounds.left
    +                    }
    +                    ALIGN_RIGHT -> {
    +                        parentContainerBounds.right - hingeBounds.right
    +                    }
    +                    else -> {
    +                        throw IllegalStateException("Unhandled condition to get height in pixel! " +
    +                            "embeddingBounds=$this taskBounds=$parentContainerBounds " +
    +                            "windowLayoutInfo=$windowLayoutInfo")
    +                    }
    +                }
    +            }
    +            else -> throw IllegalArgumentException("Unhandled width dimension=$width")
    +        }
    +    }
    +
    +    /**
    +     * Calculates [height] in pixel with [parentContainerBounds] and [windowLayoutInfo].
    +     */
    +    @Px
    +    internal fun getHeightInPixel(
    +        parentContainerBounds: Bounds,
    +        windowLayoutInfo: WindowLayoutInfo
    +    ): Int {
    +        val taskHeight = parentContainerBounds.height
    +        val heightDimension = if (shouldUseFallbackDimensionForHeight(windowLayoutInfo)) {
    +            ratio(0.5f)
    +        } else {
    +            height
    +        }
    +        when (heightDimension) {
    +            is Dimension.Ratio -> return heightDimension * taskHeight
    +            is Dimension.Pixel -> return min(taskHeight, heightDimension.value)
    +            DIMENSION_HINGE -> {
    +                // Should be verified by #shouldUseFallbackDimensionForWidth
    +                val hingeBounds = windowLayoutInfo.getOnlyFoldingFeatureOrNull()!!.bounds
    +                return when (alignment) {
    +                    ALIGN_TOP -> {
    +                        hingeBounds.top - parentContainerBounds.top
    +                    }
    +                    ALIGN_BOTTOM -> {
    +                        parentContainerBounds.bottom - hingeBounds.bottom
    +                    }
    +                    else -> {
    +                        throw IllegalStateException("Unhandled condition to get height in pixel! " +
    +                            "embeddingBounds=$this taskBounds=$parentContainerBounds " +
    +                            "windowLayoutInfo=$windowLayoutInfo")
    +                    }
    +                }
    +            }
    +            else -> throw IllegalArgumentException("Unhandled width dimension=$width")
    +        }
    +    }
    +
    +    /** The position of the bounds relative to parent window container. */
    +    class Alignment internal constructor(@IntRange(from = 0, to = 3) internal val value: Int) {
    +
    +        init { require(value in 0..3) }
    +
    +        override fun equals(other: Any?): Boolean {
    +            if (this === other) return true
    +            if (other !is Alignment) return false
    +            return value == other.value
    +        }
    +
    +        override fun hashCode(): Int {
    +            return value
    +        }
    +
    +        override fun toString(): String = when (value) {
    +            0 -> "top"
    +            1 -> "left"
    +            2 -> "bottom"
    +            3 -> "right"
    +            else -> "unknown position:$value"
    +        }
    +
    +        companion object {
    +
    +            /** Specifies that the bounds is at the top of the parent window container. */
    +            @JvmField
    +            val ALIGN_TOP = Alignment(0)
    +
    +            /** Specifies that the bounds is at the left of the parent window container. */
    +            @JvmField
    +            val ALIGN_LEFT = Alignment(1)
    +
    +            /** Specifies that the bounds is at the bottom of the parent window container. */
    +            @JvmField
    +            val ALIGN_BOTTOM = Alignment(2)
    +
    +            /** Specifies that the bounds is at the right of the parent window container. */
    +            @JvmField
    +            val ALIGN_RIGHT = Alignment(3)
    +        }
    +    }
    +
    +    /**
    +     * The dimension of the bounds, which can be represented as multiple formats:
    +     * - [DIMENSION_EXPANDED]: means the bounds' dimension fills parent window's dimension.
    +     * - in [pixel]: To specify the dimension value in pixel.
    +     * - in [ratio]: To specify the dimension that relative to the parent window container.
    +     * For example, if [width] has [ratio] value 0.6, it means the bounds' width is 0.6 to the
    +     * parent window container's width.
    +     */
    +    abstract class Dimension internal constructor(internal val description: String) {
    +
    +        override fun equals(other: Any?): Boolean {
    +            if (this === other) return true
    +            if (other !is Dimension) return false
    +            return description == other.description
    +        }
    +
    +        override fun hashCode(): Int = description.hashCode()
    +
    +        override fun toString(): String = description
    +
    +        /**
    +         * The [Dimension] represented in pixel format
    +         *
    +         * @param value The dimension length in pixel
    +         */
    +        internal class Pixel(
    +            @Px
    +            @IntRange(from = 1)
    +            internal val value: Int
    +        ) : Dimension("dimension in pixel:$value") {
    +
    +            init { require(value >= 1) { "Pixel value must be a positive integer." } }
    +
    +            internal operator fun compareTo(dimen: Int): Int = value - dimen
    +        }
    +
    +        /**
    +         * The [Dimension] represented in ratio format, which means the proportion of the parent
    +         * window dimension.
    +         *
    +         * @param value The ratio in (0.0, 1.0)
    +         */
    +        internal class Ratio(
    +            @FloatRange(from = 0.0, fromInclusive = false, to = 1.0)
    +            internal val value: Float
    +        ) : Dimension("dimension in ratio:$value") {
    +
    +            init { require(value > 0.0 && value <= 1.0) { "Ratio must be in range (0.0, 1.0]" } }
    +
    +            internal operator fun times(dimen: Int): Int = (value * dimen).toInt()
    +        }
    +
    +        companion object {
    +
    +            /** Represents this dimension follows its parent window dimension. */
    +            @JvmField
    +            val DIMENSION_EXPANDED: Dimension = Ratio(1.0f)
    +
    +            /**
    +             * Represents this dimension follows the hinge position if the current window and
    +             * device state satisfies, or fallbacks to a half of the parent task dimension,
    +             * otherwise.
    +             *
    +             * The [DIMENSION_HINGE] works only if:
    +             *
    +             * - The parent container is not in multi-window mode (e.g., split-screen mode or
    +             *   picture-in-picture mode)
    +             * - The device has a hinge or separating fold reported by
    +             *   [androidx.window.layout.FoldingFeature.isSeparating]
    +             * - The hinge or separating fold orientation matches [EmbeddingBounds.alignment]:
    +             *       - The hinge or fold orientation is vertical, and the position is
    +             *         [POSITION_LEFT] or [POSITION_RIGHT]
    +             *       - The hinge or fold orientation is horizontal, and the position is
    +             *         [POSITION_TOP] or [POSITION_BOTTOM]
    +             */
    +            @JvmField
    +            val DIMENSION_HINGE: Dimension = object : Dimension("hinge") {}
    +
    +            /**
    +             * Creates the dimension in pixel.
    +             *
    +             * If the dimension length exceeds the parent window dimension, the overlay container
    +             * will resize to fit the parent task dimension.
    +             *
    +             * @param value The dimension length in pixel
    +             */
    +            @JvmStatic
    +            fun pixel(@Px @IntRange(from = 1) value: Int): Dimension = Pixel(value)
    +
    +            /**
    +             * Creates the dimension which takes a proportion of the parent window dimension.
    +             *
    +             * @param ratio The proportion of the parent window dimension this dimension should take
    +             */
    +            @JvmStatic
    +            fun ratio(
    +                @FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = false)
    +                ratio: Float
    +            ): Dimension = Ratio(ratio)
    +        }
    +    }
    +
    +    companion object {
    +
    +        /** The bounds fills the parent window bounds */
    +        @JvmField
    +        val BOUNDS_EXPANDED = EmbeddingBounds(ALIGN_TOP, DIMENSION_EXPANDED, DIMENSION_EXPANDED)
    +
    +        /**
    +         * The bounds located on the top of the parent window, and the bounds' bottom side matches
    +         * the hinge position.
    +         */
    +        @JvmField
    +        val BOUNDS_HINGE_TOP = EmbeddingBounds(
    +            ALIGN_TOP,
    +            width = DIMENSION_EXPANDED,
    +            height = DIMENSION_HINGE
    +        )
    +
    +        /**
    +         * The bounds located on the left of the parent window, and the bounds' right side matches
    +         * the hinge position.
    +         */
    +        @JvmField
    +        val BOUNDS_HINGE_LEFT = EmbeddingBounds(
    +            ALIGN_LEFT,
    +            width = DIMENSION_HINGE,
    +            height = DIMENSION_EXPANDED
    +        )
    +
    +        /**
    +         * The bounds located on the bottom of the parent window, and the bounds' top side matches
    +         * the hinge position.
    +         */
    +        @JvmField
    +        val BOUNDS_HINGE_BOTTOM = EmbeddingBounds(
    +            ALIGN_BOTTOM,
    +            width = DIMENSION_EXPANDED,
    +            height = DIMENSION_HINGE
    +        )
    +
    +        /**
    +         * The bounds located on the right of the parent window, and the bounds' left side matches
    +         * the hinge position.
    +         */
    +        @JvmField
    +        val BOUNDS_HINGE_RIGHT = EmbeddingBounds(
    +            ALIGN_RIGHT,
    +            width = DIMENSION_HINGE,
    +            height = DIMENSION_EXPANDED
    +        )
    +
    +        /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */
    +        @VisibleForTesting
    +        internal fun translateEmbeddingBounds(
    +            embeddingBounds: EmbeddingBounds,
    +            parentContainerBounds: Bounds,
    +            windowLayoutInfo: WindowLayoutInfo,
    +        ): Bounds {
    +            if (embeddingBounds.width == DIMENSION_EXPANDED &&
    +                embeddingBounds.height == DIMENSION_EXPANDED) {
    +                // If width and height are expanded, set bounds to empty to follow the parent task
    +                // bounds.
    +                return Bounds.EMPTY_BOUNDS
    +            }
    +            // 1. Fallbacks dimensions to ratio(0.5) if they can't follow the hinge with the current
    +            //    device and window state.
    +            val width = if (embeddingBounds.shouldUseFallbackDimensionForWidth(windowLayoutInfo)) {
    +                ratio(0.5f)
    +            } else {
    +                embeddingBounds.width
    +            }
    +            val height = if (
    +                embeddingBounds.shouldUseFallbackDimensionForHeight(windowLayoutInfo)
    +            ) {
    +                ratio(0.5f)
    +            } else {
    +                embeddingBounds.height
    +            }
    +
    +            // 2. Computes dimensions to pixel values. If it just matches parent task bounds, returns
    +            //    the empty bounds to declare the bounds follow the parent task bounds.
    +            val sanitizedBounds = EmbeddingBounds(embeddingBounds.alignment, width, height)
    +            val widthInPixel = sanitizedBounds.getWidthInPixel(
    +                parentContainerBounds,
    +                windowLayoutInfo
    +            )
    +            val heightInPixel = sanitizedBounds.getHeightInPixel(
    +                parentContainerBounds,
    +                windowLayoutInfo
    +            )
    +            val taskWidth = parentContainerBounds.width
    +            val taskHeight = parentContainerBounds.height
    +
    +            if (widthInPixel == taskWidth && heightInPixel == taskHeight) {
    +                return Bounds.EMPTY_BOUNDS
    +            }
    +
    +            // 3. Offset the bounds by position:
    +            //     - For top or bottom position, the bounds should attach to the top or bottom of
    +            //       the parent task bounds and centered by the middle of the width.
    +            //     - For left or right position, the bounds should attach to the left or right of
    +            //       the parent task bounds and centered by the middle of the height.
    +            return Bounds(0, 0, widthInPixel, heightInPixel).let { bounds ->
    +                when (embeddingBounds.alignment) {
    +                    ALIGN_TOP ->
    +                        bounds.offset(((taskWidth - widthInPixel) / 2), 0)
    +                    ALIGN_LEFT ->
    +                        bounds.offset(0, ((taskHeight - heightInPixel) / 2))
    +                    ALIGN_BOTTOM ->
    +                        bounds.offset(((taskWidth - widthInPixel) / 2), taskHeight - heightInPixel)
    +                    ALIGN_RIGHT ->
    +                        bounds.offset(taskWidth - widthInPixel, ((taskHeight - heightInPixel) / 2))
    +                    else ->
    +                        throw IllegalArgumentException(
    +                            "Unknown alignment: ${embeddingBounds.alignment}"
    +                        )
    +                }
    +            }
    +        }
    +
    +        private fun Bounds.offset(dx: Int, dy: Int): Bounds = Bounds(
    +            left + dx,
    +            top + dy,
    +            right + dx,
    +            bottom + dy
    +        )
    +
    +        /** Translates [EmbeddingBounds] to pure [Rect] bounds with given [ParentContainerInfo]. */
    +        internal fun translateEmbeddingBounds(
    +            embeddingBounds: EmbeddingBounds,
    +            parentContainerInfo: ParentContainerInfo,
    +        ): Bounds = translateEmbeddingBounds(
    +            embeddingBounds,
    +            parentContainerInfo.windowBounds,
    +            parentContainerInfo.windowLayoutInfo
    +        )
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
    index 87f844d..541517e 100644
    --- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
    
    @@ -17,23 +17,26 @@
     package androidx.window.embedding
     
     import android.app.Activity
    -import android.app.ActivityOptions
     import android.content.Context
    -import android.os.IBinder
    +import android.os.Bundle
     import android.util.Log
    +import androidx.annotation.VisibleForTesting
    +import androidx.core.util.Consumer as JetpackConsumer
     import androidx.window.RequiresWindowSdkExtension
     import androidx.window.WindowSdkExtensions
     import androidx.window.core.BuildConfig
     import androidx.window.core.ConsumerAdapter
    -import androidx.window.core.ExtensionsUtil
     import androidx.window.core.VerificationMode
     import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
     import androidx.window.embedding.SplitController.SplitSupportStatus.Companion.SPLIT_AVAILABLE
     import androidx.window.extensions.WindowExtensionsProvider
     import androidx.window.extensions.core.util.function.Consumer
     import androidx.window.extensions.embedding.ActivityEmbeddingComponent
    +import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
    +import androidx.window.extensions.embedding.ActivityStackAttributes
     import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
     import java.lang.reflect.Proxy
    +import java.util.concurrent.Executor
     
     /**
      * Adapter implementation for different historical versions of activity embedding OEM interface in
    @@ -43,9 +46,16 @@
         private val embeddingExtension: ActivityEmbeddingComponent,
         private val adapter: EmbeddingAdapter,
         private val consumerAdapter: ConsumerAdapter,
    -    private val applicationContext: Context
    +    private val applicationContext: Context,
    +    @get:VisibleForTesting
    +    internal val overlayController: OverlayControllerImpl?,
    +    private val activityWindowInfoCallbackController: ActivityWindowInfoCallbackController?,
     ) : EmbeddingInterfaceCompat {
     
    +    private val windowSdkExtensions = WindowSdkExtensions.getInstance()
    +
    +    private var isCustomSplitAttributeCalculatorSet: Boolean = false
    +
         override fun setRules(rules: Set) {
             var hasSplitRule = false
             for (rule in rules) {
    @@ -71,73 +81,236 @@
         }
     
         override fun setEmbeddingCallback(embeddingCallback: EmbeddingCallbackInterface) {
    -        if (ExtensionsUtil.safeVendorApiLevel < 2) {
    -            consumerAdapter.addConsumer(
    -                embeddingExtension,
    -                List::class,
    -                "setSplitInfoCallback"
    -            ) { values ->
    -                val splitInfoList = values.filterIsInstance()
    -                embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
    +        when (windowSdkExtensions.extensionVersion) {
    +            1 -> {
    +                consumerAdapter.addConsumer(
    +                    embeddingExtension,
    +                    List::class,
    +                    "setSplitInfoCallback"
    +                ) { values ->
    +                    val splitInfoList = values.filterIsInstance()
    +                    embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
    +                }
                 }
    -        } else {
    -            val callback = Consumer> { splitInfoList ->
    -                embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
    +            in 2..4 -> {
    +                registerSplitInfoCallback(embeddingCallback)
                 }
    -            embeddingExtension.setSplitInfoCallback(callback)
    +            in 5..Int.MAX_VALUE -> {
    +                registerSplitInfoCallback(embeddingCallback)
    +
    +                // Register ActivityStack callback
    +                val activityStackCallback = Consumer> { activityStacks ->
    +                    embeddingCallback.onActivityStackChanged(adapter.translate(activityStacks))
    +                }
    +                embeddingExtension.registerActivityStackCallback(
    +                    Runnable::run,
    +                    activityStackCallback
    +                )
    +            }
             }
         }
     
    +    private fun registerSplitInfoCallback(embeddingCallback: EmbeddingCallbackInterface) {
    +        val splitInfoCallback = Consumer> { splitInfoList ->
    +            embeddingCallback.onSplitInfoChanged(adapter.translate(splitInfoList))
    +        }
    +        embeddingExtension.setSplitInfoCallback(splitInfoCallback)
    +    }
    +
         override fun isActivityEmbedded(activity: Activity): Boolean {
             return embeddingExtension.isActivityEmbedded(activity)
         }
     
    +    @RequiresWindowSdkExtension(5)
    +    override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
    +        windowSdkExtensions.requireExtensionVersion(5)
    +        return embeddingExtension.pinTopActivityStack(
    +            taskId,
    +            adapter.translateSplitPinRule(
    +                applicationContext,
    +                splitPinRule
    +            )
    +        )
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    override fun unpinTopActivityStack(taskId: Int) {
    +        windowSdkExtensions.requireExtensionVersion(5)
    +        return embeddingExtension.unpinTopActivityStack(taskId)
    +    }
    +
         @RequiresWindowSdkExtension(2)
         override fun setSplitAttributesCalculator(
             calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
         ) {
    -        WindowSdkExtensions.getInstance().requireExtensionVersion(2)
    +        windowSdkExtensions.requireExtensionVersion(2)
     
             embeddingExtension.setSplitAttributesCalculator(
                 adapter.translateSplitAttributesCalculator(calculator)
             )
    +        isCustomSplitAttributeCalculatorSet = true;
         }
     
         @RequiresWindowSdkExtension(2)
         override fun clearSplitAttributesCalculator() {
    -        WindowSdkExtensions.getInstance().requireExtensionVersion(2)
    +        windowSdkExtensions.requireExtensionVersion(2)
     
             embeddingExtension.clearSplitAttributesCalculator()
    +        isCustomSplitAttributeCalculatorSet = false
    +        setDefaultSplitAttributeCalculatorIfNeeded()
         }
     
    -    @RequiresWindowSdkExtension(3)
    -    override fun invalidateTopVisibleSplitAttributes() {
    -        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
    +    @RequiresWindowSdkExtension(5)
    +    override fun finishActivityStacks(activityStacks: Set) {
    +        windowSdkExtensions.requireExtensionVersion(5)
    +
    +        embeddingExtension.finishActivityStacksWithTokens(
    +            activityStacks.mapTo(mutableSetOf()) { it.getToken() }
    +        )
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
    +        windowSdkExtensions.requireExtensionVersion(5)
    +        adapter.embeddingConfiguration = embeddingConfig
    +        setDefaultSplitAttributeCalculatorIfNeeded()
     
             embeddingExtension.invalidateTopVisibleSplitAttributes()
         }
     
    +    private fun setDefaultSplitAttributeCalculatorIfNeeded() {
    +        // Setting a default SplitAttributeCalculator if the EmbeddingConfiguration is set,
    +        // in order to ensure the dimAreaBehavior in the SplitAttribute is up-to-date.
    +        if (windowSdkExtensions.extensionVersion >= 5 && !isCustomSplitAttributeCalculatorSet &&
    +            adapter.embeddingConfiguration != null) {
    +            embeddingExtension.setSplitAttributesCalculator { params ->
    +                adapter.translateSplitAttributes(adapter.translate(params.defaultSplitAttributes))
    +            }
    +        }
    +    }
    +
    +    @RequiresWindowSdkExtension(3)
    +    override fun invalidateVisibleActivityStacks() {
    +        windowSdkExtensions.requireExtensionVersion(3)
    +
    +        embeddingExtension.invalidateVisibleActivityStacks()
    +    }
    +
    +    /**
    +     * Updates top [activityStacks][ActivityStack] layouts, which will trigger [SplitAttributes]
    +     * calculator and [ActivityStackAttributes] calculator if set.
    +     */
    +    private fun ActivityEmbeddingComponent.invalidateVisibleActivityStacks() {
    +        // Note that this API also updates overlay container regardless of its naming.
    +        invalidateTopVisibleSplitAttributes()
    +    }
    +
    +    @Suppress("Deprecation") // To compat with device with extension version 3 and 4.
         @RequiresWindowSdkExtension(3)
         override fun updateSplitAttributes(
             splitInfo: SplitInfo,
             splitAttributes: SplitAttributes
         ) {
    -        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
    +        windowSdkExtensions.requireExtensionVersion(3)
     
    -        embeddingExtension.updateSplitAttributes(
    -            splitInfo.token,
    -            adapter.translateSplitAttributes(splitAttributes)
    -        )
    +        if (windowSdkExtensions.extensionVersion >= 5) {
    +            embeddingExtension.updateSplitAttributes(
    +                splitInfo.getToken(),
    +                adapter.translateSplitAttributes(splitAttributes)
    +            )
    +        } else {
    +            embeddingExtension.updateSplitAttributes(
    +                splitInfo.getBinder(),
    +                adapter.translateSplitAttributes(splitAttributes)
    +            )
    +        }
         }
     
    -    @RequiresWindowSdkExtension(3)
    +    @RequiresWindowSdkExtension(5)
         override fun setLaunchingActivityStack(
    -        options: ActivityOptions,
    -        token: IBinder
    -    ): ActivityOptions {
    -        WindowSdkExtensions.getInstance().requireExtensionVersion(3)
    +        options: Bundle,
    +        activityStack: ActivityStack
    +    ): Bundle {
    +        windowSdkExtensions.requireExtensionVersion(5)
     
    -        return embeddingExtension.setLaunchingActivityStack(options, token)
    +        ActivityEmbeddingOptionsImpl.setActivityStackToken(options, activityStack.getToken())
    +        return options
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun setOverlayCreateParams(
    +        options: Bundle,
    +        overlayCreateParams: OverlayCreateParams
    +    ): Bundle = options.apply {
    +        ActivityEmbeddingOptionsImpl.setOverlayCreateParams(options, overlayCreateParams)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun setOverlayAttributesCalculator(
    +        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
    +    ) {
    +        windowSdkExtensions.requireExtensionVersion(6)
    +
    +        overlayController!!.overlayAttributesCalculator = calculator
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun clearOverlayAttributesCalculator() {
    +        windowSdkExtensions.requireExtensionVersion(6)
    +
    +        overlayController!!.overlayAttributesCalculator = null
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
    +        windowSdkExtensions.requireExtensionVersion(6)
    +
    +        overlayController!!.updateOverlayAttributes(overlayTag, overlayAttributes)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun addOverlayInfoCallback(
    +        overlayTag: String,
    +        executor: Executor,
    +        overlayInfoCallback: JetpackConsumer,
    +    ) {
    +        overlayController?.addOverlayInfoCallback(
    +            overlayTag,
    +            executor,
    +            overlayInfoCallback,
    +        ) ?: apply {
    +            Log.w(TAG, "overlayInfo is not supported on device less than version 5")
    +
    +            overlayInfoCallback.accept(
    +                OverlayInfo(
    +                    overlayTag,
    +                    currentOverlayAttributes = null,
    +                    activityStack = null,
    +                )
    +            )
    +        }
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer) {
    +        overlayController?.removeOverlayInfoCallback(overlayInfoCallback)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun addEmbeddedActivityWindowInfoCallbackForActivity(
    +        activity: Activity,
    +        callback: JetpackConsumer
    +    ) {
    +        activityWindowInfoCallbackController?.addCallback(activity, callback) ?: apply {
    +            Log.w(TAG, "EmbeddedActivityWindowInfo is not supported on device less than version 6")
    +        }
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
    +        callback: JetpackConsumer
    +    ) {
    +        activityWindowInfoCallbackController?.removeCallback(callback)
         }
     
         companion object {
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt
    new file mode 100644
    index 0000000..0e2e962
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingConfiguration.kt
    
    @@ -0,0 +1,126 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import androidx.annotation.IntRange
    +import androidx.window.RequiresWindowSdkExtension
    +
    +/**
    + * Configurations of Activity Embedding environment that defines how the
    + * embedded Activities behave.
    + *
    + * @see ActivityEmbeddingController.setEmbeddingConfiguration
    + * @property dimAreaBehavior The requested dim area behavior.
    + * @constructor The [EmbeddingConfiguration] constructor. The properties are undefined
    + *              if not specified.
    + */
    +class EmbeddingConfiguration @JvmOverloads constructor(
    +    @RequiresWindowSdkExtension(5)
    +    val dimAreaBehavior: DimAreaBehavior = DimAreaBehavior.UNDEFINED
    +) {
    +    /**
    +     * The area of dimming to apply.
    +     *
    +     * @see [android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND]
    +     */
    +    class DimAreaBehavior private constructor(@IntRange(from = 0, to = 2) internal val value: Int) {
    +        companion object {
    +            /**
    +             * The dim area is not defined.
    +             *
    +             * This is the default value while building a [EmbeddingConfiguration]. This would
    +             * also keep the existing dim area configuration of the current Activity Embedding
    +             * environment unchanged when [ActivityEmbeddingController.setEmbeddingConfiguration]
    +             * is called.
    +             *
    +             * @see ActivityEmbeddingController.setEmbeddingConfiguration
    +             */
    +            @JvmField
    +            val UNDEFINED = DimAreaBehavior(0)
    +
    +            /**
    +             * The dim effect is applying on the [ActivityStack] of the Activity window when needed.
    +             * If the [ActivityStack] is split and displayed side-by-side with another
    +             * [ActivityStack], the dim effect is applying only on the [ActivityStack] of the
    +             * requested Activity.
    +             */
    +            @JvmField
    +            val ON_ACTIVITY_STACK = DimAreaBehavior(1)
    +
    +            /**
    +             * The dimming effect is applying on the area of the whole Task when needed. If the
    +             * embedded transparent activity is split and displayed side-by-side with another
    +             * activity, the dim effect is applying on the Task, which across over the two
    +             * [ActivityStack]s.
    +             *
    +             * This is the default dim area configuration of the Activity Embedding environment,
    +             * before the [DimAreaBehavior] is explicitly set by
    +             * [ActivityEmbeddingController.setEmbeddingConfiguration].
    +             */
    +            @JvmField
    +            val ON_TASK = DimAreaBehavior(2)
    +        }
    +
    +        override fun toString(): String {
    +            return "DimAreaBehavior=" + when (value) {
    +                0 -> "UNDEFINED"
    +                1 -> "ON_ACTIVITY_STACK"
    +                2 -> "ON_TASK"
    +                else -> "UNKNOWN"
    +            }
    +        }
    +    }
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) return true
    +        if (other !is EmbeddingConfiguration) return false
    +
    +        if (dimAreaBehavior != other.dimAreaBehavior) return false;
    +        return true
    +    }
    +
    +    override fun hashCode(): Int {
    +        return dimAreaBehavior.hashCode()
    +    }
    +
    +    override fun toString(): String =
    +        "EmbeddingConfiguration{$dimAreaBehavior}"
    +
    +    /**
    +     * Builder for creating an instance of [EmbeddingConfiguration].
    +     */
    +    class Builder {
    +        private var mDimAreaBehavior = DimAreaBehavior.UNDEFINED
    +
    +        /**
    +         * Sets the dim area behavior. By default, the [DimAreaBehavior.UNDEFINED] is used if not
    +         * set.
    +         *
    +         * @param area The dim area.
    +         * @return This [Builder]
    +         */
    +        @SuppressWarnings("MissingGetterMatchingBuilder")
    +        fun setDimAreaBehavior(area: DimAreaBehavior): Builder = apply { mDimAreaBehavior = area }
    +
    +        /**
    +         * Builds a[EmbeddingConfiguration] instance.
    +         *
    +         * @return The new [EmbeddingConfiguration] instance.
    +         */
    +        fun build(): EmbeddingConfiguration = EmbeddingConfiguration(mDimAreaBehavior)
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
    index 87b899d..d414621 100644
    --- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
    
    @@ -17,10 +17,11 @@
     package androidx.window.embedding
     
     import android.app.Activity
    -import android.app.ActivityOptions
    -import android.os.IBinder
    +import android.os.Bundle
    +import androidx.core.util.Consumer
     import androidx.window.RequiresWindowSdkExtension
     import androidx.window.extensions.embedding.ActivityEmbeddingComponent
    +import java.util.concurrent.Executor
     
     /**
      * Adapter interface for different historical versions of activity embedding OEM interface in
    @@ -34,10 +35,18 @@
     
         interface EmbeddingCallbackInterface {
             fun onSplitInfoChanged(splitInfo: List)
    +
    +        fun onActivityStackChanged(activityStacks: List)
         }
     
         fun isActivityEmbedded(activity: Activity): Boolean
     
    +    @RequiresWindowSdkExtension(5)
    +    fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun unpinTopActivityStack(taskId: Int)
    +
         @RequiresWindowSdkExtension(2)
         fun setSplitAttributesCalculator(
             calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
    @@ -46,12 +55,53 @@
         @RequiresWindowSdkExtension(2)
         fun clearSplitAttributesCalculator()
     
    -    @RequiresWindowSdkExtension(3)
    -    fun setLaunchingActivityStack(options: ActivityOptions, token: IBinder): ActivityOptions
    +    @RequiresWindowSdkExtension(5)
    +    fun setLaunchingActivityStack(options: Bundle, activityStack: ActivityStack): Bundle
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun setOverlayCreateParams(options: Bundle, overlayCreateParams: OverlayCreateParams): Bundle
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun finishActivityStacks(activityStacks: Set)
    +
    +    @RequiresWindowSdkExtension(5)
    +    fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration)
     
         @RequiresWindowSdkExtension(3)
    -    fun invalidateTopVisibleSplitAttributes()
    +    fun invalidateVisibleActivityStacks()
     
         @RequiresWindowSdkExtension(3)
         fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes)
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun setOverlayAttributesCalculator(
    +        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
    +    )
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun clearOverlayAttributesCalculator()
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes)
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun addOverlayInfoCallback(
    +        overlayTag: String,
    +        executor: Executor,
    +        overlayInfoCallback: Consumer,
    +    )
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun removeOverlayInfoCallback(overlayInfoCallback: Consumer)
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun addEmbeddedActivityWindowInfoCallbackForActivity(
    +        activity: Activity,
    +        callback: Consumer
    +    )
    +
    +    @RequiresWindowSdkExtension(6)
    +    fun removeEmbeddedActivityWindowInfoCallbackForActivity(
    +        callback: Consumer
    +    )
     }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
    index 33ea2cb..5d5c46b 100644
    --- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
    
    @@ -17,11 +17,10 @@
     package androidx.window.embedding
     
     import android.app.Activity
    -import android.app.ActivityOptions
     import android.content.Context
     import android.content.pm.PackageManager
     import android.os.Build
    -import android.os.IBinder
    +import android.os.Bundle
     import android.util.Log
     import androidx.annotation.DoNotInline
     import androidx.annotation.GuardedBy
    @@ -31,6 +30,7 @@
     import androidx.core.util.Consumer
     import androidx.window.RequiresWindowSdkExtension
     import androidx.window.WindowProperties
    +import androidx.window.WindowSdkExtensions
     import androidx.window.core.BuildConfig
     import androidx.window.core.ConsumerAdapter
     import androidx.window.core.ExtensionsUtil
    @@ -52,11 +52,11 @@
     
         @VisibleForTesting
         val splitChangeCallbacks: CopyOnWriteArrayList
    -    private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()
    +    private val embeddingCallback = EmbeddingCallbackImpl()
     
         init {
             splitChangeCallbacks = CopyOnWriteArrayList()
    -        embeddingExtension?.setEmbeddingCallback(splitInfoEmbeddingCallback)
    +        embeddingExtension?.setEmbeddingCallback(embeddingCallback)
         }
     
         companion object {
    @@ -95,11 +95,23 @@
                         EmbeddingCompat.isEmbeddingAvailable()
                     ) {
                         impl = EmbeddingBackend::class.java.classLoader?.let { loader ->
    +                        val embeddingExtension = EmbeddingCompat.embeddingComponent()
    +                        val adapter = EmbeddingAdapter(PredicateAdapter(loader))
                             EmbeddingCompat(
    -                            EmbeddingCompat.embeddingComponent(),
    -                            EmbeddingAdapter(PredicateAdapter(loader)),
    +                            embeddingExtension,
    +                            adapter,
                                 ConsumerAdapter(loader),
    -                            applicationContext
    +                            applicationContext,
    +                            if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
    +                                OverlayControllerImpl(embeddingExtension, adapter)
    +                            } else {
    +                                null
    +                            },
    +                            if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
    +                                ActivityWindowInfoCallbackController(embeddingExtension)
    +                            } else {
    +                                null
    +                            },
                             )
                         }
                         // TODO(b/190433400): Check API conformance
    @@ -278,11 +290,7 @@
     
                 val callbackWrapper = SplitListenerWrapper(activity, executor, callback)
                 splitChangeCallbacks.add(callbackWrapper)
    -            if (splitInfoEmbeddingCallback.lastInfo != null) {
    -                callbackWrapper.accept(splitInfoEmbeddingCallback.lastInfo!!)
    -            } else {
    -                callbackWrapper.accept(emptyList())
    -            }
    +            callbackWrapper.accept(embeddingCallback.lastInfo)
             }
         }
     
    @@ -300,17 +308,24 @@
         }
     
         /**
    -     * Extension callback implementation of the split information. Keeps track of last reported
    +     * Extension callback implementation of the embedding information. Keeps track of last reported
          * values.
          */
         internal inner class EmbeddingCallbackImpl : EmbeddingCallbackInterface {
    -        var lastInfo: List? = null
    +        var lastInfo: List = emptyList()
    +
    +        var lastActivityStacks: List = emptyList()
    +
             override fun onSplitInfoChanged(splitInfo: List) {
                 lastInfo = splitInfo
                 for (callbackWrapper in splitChangeCallbacks) {
                     callbackWrapper.accept(splitInfo)
                 }
             }
    +
    +        override fun onActivityStackChanged(activityStacks: List) {
    +            lastActivityStacks = activityStacks
    +        }
         }
     
         private fun areExtensionsAvailable(): Boolean {
    @@ -337,6 +352,16 @@
             return embeddingExtension?.isActivityEmbedded(activity) ?: false
         }
     
    +    @RequiresWindowSdkExtension(5)
    +    override fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
    +        return embeddingExtension?.pinTopActivityStack(taskId, splitPinRule) ?: false
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    override fun unpinTopActivityStack(taskId: Int) {
    +        embeddingExtension?.unpinTopActivityStack(taskId)
    +    }
    +
         @RequiresWindowSdkExtension(2)
         override fun setSplitAttributesCalculator(
             calculator: (SplitAttributesCalculatorParams) -> SplitAttributes
    @@ -353,33 +378,55 @@
             }
         }
     
    -    override fun getActivityStack(activity: Activity): ActivityStack? {
    +    override fun getActivityStack(activity: Activity): ActivityStack? =
             globalLock.withLock {
    -            val lastInfo: List = splitInfoEmbeddingCallback.lastInfo ?: return null
    -            for (info in lastInfo) {
    -                if (activity !in info) {
    -                    continue
    -                }
    -                if (activity in info.primaryActivityStack) {
    -                    return info.primaryActivityStack
    -                }
    -                if (activity in info.secondaryActivityStack) {
    -                    return info.secondaryActivityStack
    -                }
    -            }
    -            return null
    +            embeddingCallback.lastActivityStacks.find { activityStack ->
    +                activity in activityStack
    +            } ?: getActivityStackFromSplitInfoList(activity)
             }
    +
    +    @GuardedBy("globalLock")
    +    private fun getActivityStackFromSplitInfoList(activity: Activity): ActivityStack? {
    +        for (info in embeddingCallback.lastInfo) {
    +            if (activity !in info) {
    +                continue
    +            }
    +            if (activity in info.primaryActivityStack) {
    +                return info.primaryActivityStack
    +            }
    +            if (activity in info.secondaryActivityStack) {
    +                return info.secondaryActivityStack
    +            }
    +        }
    +        return null
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    override fun setLaunchingActivityStack(
    +        options: Bundle,
    +        activityStack: ActivityStack
    +    ): Bundle = embeddingExtension?.setLaunchingActivityStack(options, activityStack) ?: options
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun setOverlayCreateParams(
    +        options: Bundle,
    +        overlayCreateParams: OverlayCreateParams,
    +    ): Bundle = embeddingExtension?.setOverlayCreateParams(options, overlayCreateParams)
    +        ?: options
    +
    +    @RequiresWindowSdkExtension(5)
    +    override fun finishActivityStacks(activityStacks: Set) {
    +        embeddingExtension?.finishActivityStacks(activityStacks)
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    override fun setEmbeddingConfiguration(embeddingConfig: EmbeddingConfiguration) {
    +        embeddingExtension?.setEmbeddingConfiguration(embeddingConfig)
         }
     
         @RequiresWindowSdkExtension(3)
    -    override fun setLaunchingActivityStack(
    -        options: ActivityOptions,
    -        token: IBinder
    -    ): ActivityOptions = embeddingExtension?.setLaunchingActivityStack(options, token) ?: options
    -
    -    @RequiresWindowSdkExtension(3)
    -    override fun invalidateTopVisibleSplitAttributes() {
    -        embeddingExtension?.invalidateTopVisibleSplitAttributes()
    +    override fun invalidateVisibleActivityStacks() {
    +        embeddingExtension?.invalidateVisibleActivityStacks()
         }
     
         @RequiresWindowSdkExtension(3)
    @@ -390,6 +437,60 @@
             embeddingExtension?.updateSplitAttributes(splitInfo, splitAttributes)
         }
     
    +    @RequiresWindowSdkExtension(6)
    +    override fun setOverlayAttributesCalculator(
    +        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
    +    ) {
    +        embeddingExtension?.setOverlayAttributesCalculator(calculator)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun clearOverlayAttributesCalculator() {
    +        embeddingExtension?.clearOverlayAttributesCalculator()
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
    +        embeddingExtension?.updateOverlayAttributes(overlayTag, overlayAttributes)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun addOverlayInfoCallback(
    +        overlayTag: String,
    +        executor: Executor,
    +        overlayInfoCallback: Consumer,
    +    ) {
    +        embeddingExtension?.addOverlayInfoCallback(overlayTag, executor, overlayInfoCallback)
    +        // Send an empty OverlayInfo if the extension does not exist.
    +            ?: overlayInfoCallback.accept(
    +                OverlayInfo(
    +                    overlayTag,
    +                    currentOverlayAttributes = null,
    +                    activityStack = null,
    +                )
    +            )
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun removeOverlayInfoCallback(overlayInfoCallback: Consumer) {
    +        embeddingExtension?.removeOverlayInfoCallback(overlayInfoCallback)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun addEmbeddedActivityWindowInfoCallbackForActivity(
    +        activity: Activity,
    +        callback: Consumer
    +    ) {
    +        embeddingExtension?.addEmbeddedActivityWindowInfoCallbackForActivity(activity, callback)
    +    }
    +
    +    @RequiresWindowSdkExtension(6)
    +    override fun removeEmbeddedActivityWindowInfoCallbackForActivity(
    +        callback: Consumer
    +    ) {
    +        embeddingExtension?.removeEmbeddedActivityWindowInfoCallbackForActivity(callback)
    +    }
    +
         @RequiresApi(31)
         private object Api31Impl {
             @DoNotInline
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt b/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt
    new file mode 100644
    index 0000000..193d962
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/OverlayAttributes.kt
    
    @@ -0,0 +1,62 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +/**
    + * The attributes to describe how an overlay container should look like.
    + *
    + * @property bounds The overlay container's [EmbeddingBounds], which defaults to
    + * [EmbeddingBounds.BOUNDS_EXPANDED] if not specified.
    + * @constructor creates an overlay attributes.
    + */
    +class OverlayAttributes @JvmOverloads constructor(
    +    val bounds: EmbeddingBounds = EmbeddingBounds.BOUNDS_EXPANDED
    +) {
    +
    +    override fun toString(): String =
    +        "${OverlayAttributes::class.java.simpleName}: {" +
    +            "bounds=$bounds" +
    +            "}"
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) return true
    +        if (other !is OverlayAttributes) return false
    +        return bounds == other.bounds
    +    }
    +
    +    override fun hashCode(): Int = bounds.hashCode()
    +
    +    /** The [OverlayAttributes] builder. */
    +    class Builder {
    +
    +        private var bounds = EmbeddingBounds.BOUNDS_EXPANDED
    +
    +        /**
    +         * Sets the overlay bounds, which defaults to [EmbeddingBounds.BOUNDS_EXPANDED] if not
    +         * specified.
    +         *
    +         * @param bounds The [EmbeddingBounds] of the overlay [ActivityStack].
    +         * @return The [OverlayAttributes] builder.
    +         */
    +        fun setBounds(bounds: EmbeddingBounds): Builder = apply {
    +            this.bounds = bounds
    +        }
    +
    +        /** Builds [OverlayAttributes]. */
    +        fun build(): OverlayAttributes = OverlayAttributes(bounds)
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt b/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt
    new file mode 100644
    index 0000000..4dcda6d
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/OverlayAttributesCalculatorParams.kt
    
    @@ -0,0 +1,53 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.content.res.Configuration
    +import androidx.window.layout.WindowLayoutInfo
    +import androidx.window.layout.WindowMetrics
    +
    +/**
    + * The parameter container used to report the current device and window state in
    + * [OverlayController.setOverlayAttributesCalculator] and references the corresponding overlay
    + * [ActivityStack] by [overlayTag].
    + */
    +class OverlayAttributesCalculatorParams internal constructor(
    +    /** The parent container's [WindowMetrics] */
    +    val parentWindowMetrics: WindowMetrics,
    +    /** The parent container's [Configuration] */
    +    val parentConfiguration: Configuration,
    +    /** The parent container's [WindowLayoutInfo] */
    +    val parentWindowLayoutInfo: WindowLayoutInfo,
    +    /**
    +     * The unique identifier of the overlay [ActivityStack] specified by [OverlayCreateParams.tag]
    +     */
    +    val overlayTag: String,
    +    /**
    +     * The overlay [ActivityStack]'s [OverlayAttributes] specified by [overlayTag], which is the
    +     * [OverlayAttributes] that is not calculated by calculator. It should be either initialized by
    +     * [OverlayCreateParams.overlayAttributes] or [OverlayController.updateOverlayAttributes].
    +     */
    +    val defaultOverlayAttributes: OverlayAttributes,
    +) {
    +    override fun toString(): String =
    +        "${OverlayAttributesCalculatorParams::class.java}:{" +
    +            "parentWindowMetrics=$parentWindowMetrics" +
    +            "parentConfiguration=$parentConfiguration" +
    +            "parentWindowLayoutInfo=$parentWindowLayoutInfo" +
    +            "overlayTag=$overlayTag" +
    +            "defaultOverlayAttributes=$defaultOverlayAttributes"
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayController.kt b/window/window/src/main/java/androidx/window/embedding/OverlayController.kt
    new file mode 100644
    index 0000000..fef66ad
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/OverlayController.kt
    
    @@ -0,0 +1,151 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.app.Activity
    +import android.content.Context
    +import android.os.Bundle
    +import androidx.annotation.VisibleForTesting
    +import androidx.core.util.Consumer
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import kotlinx.coroutines.channels.awaitClose
    +import kotlinx.coroutines.flow.Flow
    +import kotlinx.coroutines.flow.callbackFlow
    +
    +/**
    + * The controller to manage overlay [ActivityStack], which is launched by
    + * the activityOptions that [setOverlayCreateParams].
    + *
    + * See linked sample below for how to launch an [android.app.Activity] into an overlay
    + * [ActivityStack].
    + *
    + * Supported operations are:
    + * - [setOverlayAttributesCalculator] to update overlay presentation with device or window state and
    + *   [OverlayCreateParams.tag].
    + *
    + * @sample androidx.window.samples.embedding.launchOverlayActivityStackSample
    + */
    +class OverlayController @VisibleForTesting internal constructor(
    +    private val backend: EmbeddingBackend
    +) {
    +
    +    @RequiresWindowSdkExtension(6)
    +    internal fun setOverlayCreateParams(
    +        options: Bundle,
    +        overlayCreateParams: OverlayCreateParams,
    +    ): Bundle = backend.setOverlayCreateParams(options, overlayCreateParams)
    +
    +    /**
    +     * Sets an overlay calculator function to update overlay presentation with device or window
    +     * state and [OverlayCreateParams.tag].
    +     *
    +     * Overlay calculator function is triggered with following scenarios:
    +     * - An overlay [ActivityStack] is launched.
    +     * - The parent task configuration changes. i.e. orientation change, enter/exit multi-window
    +     *   mode or resize apps in multi-window mode.
    +     * - Device folding state changes.
    +     * - Device is attached to an external display and the app is forwarded to that display.
    +     *
    +     * If there's no [calculator] set, the overlay presentation will be calculated with
    +     * the previous set [OverlayAttributes], either from [OverlayCreateParams] to initialize
    +     * the overlay container, or from the runtime API to update the overlay container's
    +     * [OverlayAttributes].
    +     *
    +     * See the sample linked below for how to use [OverlayAttributes] calculator
    +     *
    +     * @param calculator The overlay calculator function to compute [OverlayAttributes] by
    +     *   [OverlayAttributesCalculatorParams]. It will replace the previously set if it exists.
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    +     *   is less than 6.
    +     * @sample androidx.window.samples.embedding.overlayAttributesCalculatorSample
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun setOverlayAttributesCalculator(
    +        calculator: (OverlayAttributesCalculatorParams) -> OverlayAttributes
    +    ) {
    +        backend.setOverlayAttributesCalculator(calculator)
    +    }
    +
    +    /**
    +     * Clears the overlay calculator function previously set by [setOverlayAttributesCalculator].
    +     *
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    +     *                                       is less than 6.
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun clearOverlayAttributesCalculator() {
    +        backend.clearOverlayAttributesCalculator()
    +    }
    +
    +    /**
    +     * Updates [OverlayAttributes] of the overlay [ActivityStack] specified by [overlayTag].
    +     * It's no op if there's no such overlay [ActivityStack] associated with [overlayTag].
    +     *
    +     * If an [OverlayAttributes] calculator function is specified, the updated [overlayAttributes]
    +     * will be passed by [OverlayAttributesCalculatorParams.defaultOverlayAttributes] when the
    +     * calculator function applies to the overlay [ActivityStack] specified by [overlayTag].
    +     *
    +     * In most cases it is suggested to use
    +     * [ActivityEmbeddingController.invalidateVisibleActivityStacks] if a calculator has been set
    +     * through [OverlayController.setOverlayAttributesCalculator].
    +     *
    +     * @param overlayTag The overlay [ActivityStack]'s tag
    +     * @param overlayAttributes The [OverlayAttributes] to update
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    +     *                                       is less than 6.
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun updateOverlayAttributes(overlayTag: String, overlayAttributes: OverlayAttributes) {
    +        backend.updateOverlayAttributes(overlayTag, overlayAttributes)
    +    }
    +
    +    /**
    +     * A [Flow] of [OverlayInfo] that [overlayTag] is associated with.
    +     *
    +     * If there's an active overlay [ActivityStack] associated with [overlayTag], it will be
    +     * reported in [OverlayInfo.activityStack]. Otherwise, [OverlayInfo.activityStack] is `null`.
    +     *
    +     * Note that launching an overlay [ActivityStack] only supports on the device with
    +     * [WindowSdkExtensions.extensionVersion] equal to or larger than 6.
    +     * If [WindowSdkExtensions.extensionVersion] is less than 6, this flow will always
    +     * report [OverlayInfo] without associated [OverlayInfo.activityStack].
    +     *
    +     * @param overlayTag The overlay [ActivityStack]'s tag which is set through
    +     * [OverlayCreateParams]
    +     * @return a [Flow] of [OverlayInfo] this [overlayTag] is associated with
    +     */
    +    @RequiresWindowSdkExtension(6)
    +    fun overlayInfo(overlayTag: String): Flow = callbackFlow {
    +        val listener = Consumer { info: OverlayInfo -> trySend(info) }
    +        backend.addOverlayInfoCallback(overlayTag, Runnable::run, listener)
    +        awaitClose { backend.removeOverlayInfoCallback(listener) }
    +    }
    +
    +    companion object {
    +        /**
    +         * Obtains an instance of [OverlayController].
    +         *
    +         * @param context the [Context] to initialize the controller with
    +         */
    +        @JvmStatic
    +        fun getInstance(context: Context): OverlayController {
    +            val backend = EmbeddingBackend.getInstance(context)
    +            return OverlayController(backend)
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt b/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt
    new file mode 100644
    index 0000000..3a38590
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/OverlayControllerImpl.kt
    
    @@ -0,0 +1,296 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.content.res.Configuration
    +import android.util.ArrayMap
    +import androidx.annotation.GuardedBy
    +import androidx.annotation.VisibleForTesting
    +import androidx.core.util.Consumer as JetpackConsumer
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.embedding.ActivityEmbeddingOptionsImpl.getOverlayAttributes
    +import androidx.window.extensions.core.util.function.Consumer
    +import androidx.window.extensions.embedding.ActivityEmbeddingComponent
    +import androidx.window.extensions.embedding.ActivityStack
    +import androidx.window.extensions.embedding.ActivityStackAttributes
    +import androidx.window.extensions.embedding.ParentContainerInfo
    +import androidx.window.layout.WindowLayoutInfo
    +import androidx.window.layout.WindowMetrics
    +import androidx.window.layout.WindowMetricsCalculator
    +import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
    +import androidx.window.layout.util.DensityCompatHelper
    +import java.util.concurrent.Executor
    +import java.util.concurrent.locks.ReentrantLock
    +import kotlin.concurrent.withLock
    +
    +/**
    + * The core implementation of [OverlayController] APIs, which is implemented by [ActivityStack]
    + * operations in WM Extensions.
    + */
    +@Suppress("NewApi") // Suppress #translateWindowMetrics, which requires R.
    +@RequiresWindowSdkExtension(6)
    +internal open class OverlayControllerImpl(
    +    private val embeddingExtension: ActivityEmbeddingComponent,
    +    private val adapter: EmbeddingAdapter,
    +) {
    +    private val globalLock = ReentrantLock()
    +
    +    @GuardedBy("globalLock")
    +    internal var overlayAttributesCalculator:
    +        ((OverlayAttributesCalculatorParams) -> OverlayAttributes)? = null
    +        get() = globalLock.withLock { field }
    +        set(value) { globalLock.withLock { field = value } }
    +
    +    /**
    +     * Mapping between the overlay container tag and its default [OverlayAttributes].
    +     * It's to record the [OverlayAttributes] updated through [updateOverlayAttributes] and
    +     * report in [OverlayAttributesCalculatorParams].
    +     */
    +    @GuardedBy("globalLock")
    +    private val overlayTagToDefaultAttributesMap: MutableMap = ArrayMap()
    +
    +    /**
    +     * Mapping between the overlay container tag and its current [OverlayAttributes] to provide
    +     * the [OverlayInfo] updates.
    +     */
    +    @GuardedBy("globalLock")
    +    private val overlayTagToCurrentAttributesMap = ArrayMap()
    +
    +    @GuardedBy("globalLock")
    +    private val overlayTagToContainerMap = ArrayMap()
    +
    +    /**
    +     * The mapping from [OverlayInfo] callback to [activityStacks][ActivityStack] callback.
    +     */
    +    @GuardedBy("globalLock")
    +    private val overlayInfoToActivityStackCallbackMap =
    +        ArrayMap, Consumer>>()
    +
    +    init {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(6)
    +
    +        embeddingExtension.setActivityStackAttributesCalculator { params ->
    +            globalLock.withLock {
    +                val parentContainerInfo = params.parentContainerInfo
    +                val density = DensityCompatHelper.getInstance()
    +                    .density(parentContainerInfo.configuration, parentContainerInfo.windowMetrics)
    +                val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(
    +                    parentContainerInfo.windowMetrics, density
    +                )
    +                val overlayAttributes = calculateOverlayAttributes(
    +                    params.activityStackTag,
    +                    params.launchOptions.getOverlayAttributes(),
    +                    WindowMetricsCalculator.translateWindowMetrics(
    +                        params.parentContainerInfo.windowMetrics, density
    +                    ),
    +                    params.parentContainerInfo.configuration,
    +                    ExtensionsWindowLayoutInfoAdapter.translate(
    +                        windowMetrics,
    +                        parentContainerInfo.windowLayoutInfo
    +                    ),
    +                )
    +                return@setActivityStackAttributesCalculator overlayAttributes
    +                    .toActivityStackAttributes(parentContainerInfo)
    +            }
    +        }
    +
    +        embeddingExtension.registerActivityStackCallback(Runnable::run) { activityStacks ->
    +            globalLock.withLock {
    +                val lastOverlayTags = overlayTagToContainerMap.keys
    +
    +                overlayTagToContainerMap.clear()
    +                overlayTagToContainerMap.putAll(
    +                    activityStacks.getOverlayContainers().map { overlayContainer ->
    +                        Pair(overlayContainer.tag!!, overlayContainer)
    +                    }
    +                )
    +
    +                cleanUpDismissedOverlayContainerRecords(lastOverlayTags)
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Clean up records associated with dismissed overlay [activityStacks][ActivityStack] when
    +     * there's a [ActivityStack] state update.
    +     *
    +     * The dismissed overlay [activityStacks][ActivityStack] are identified by comparing the
    +     * differences of [ActivityStack] state before and after update.
    +     *
    +     * @param lastOverlayTags Overlay containers' tag before applying [ActivityStack] state update.
    +     */
    +    @GuardedBy("globalLock")
    +    private fun cleanUpDismissedOverlayContainerRecords(lastOverlayTags: Set) {
    +        if (lastOverlayTags.isEmpty()) {
    +            // If there's no last overlay container, return.
    +            return
    +        }
    +
    +        val dismissedOverlayTags = ArrayList()
    +        val currentOverlayTags = overlayTagToContainerMap.keys
    +
    +        for (overlayTag in lastOverlayTags) {
    +            if (overlayTag !in currentOverlayTags &&
    +                // If an overlay activityStack is not in the current overlay container list, check
    +                // whether the activityStack does really not exist in WM Extensions in case
    +                // an overlay container is just launched, but th WM Jetpack hasn't received the
    +                // update yet.
    +                embeddingExtension.getActivityStackToken(overlayTag) == null
    +            ) {
    +                dismissedOverlayTags.add(overlayTag)
    +            }
    +        }
    +
    +        for (overlayTag in dismissedOverlayTags) {
    +            overlayTagToDefaultAttributesMap.remove(overlayTag)
    +            overlayTagToCurrentAttributesMap.remove(overlayTag)
    +        }
    +    }
    +
    +    /**
    +     * Calculates the [OverlayAttributes] to report to the [ActivityStackAttributes] calculator.
    +     *
    +     * The calculator then computes [ActivityStackAttributes] for rendering the overlay
    +     * [ActivityStack].
    +     *
    +     * @param tag The overlay [ActivityStack].
    +     * @param initialOverlayAttrs The [OverlayCreateParams.overlayAttributes] that used to launching
    +     * this overlay [ActivityStack]
    +     * @param windowMetrics The parent window container's [WindowMetrics]
    +     * @param configuration The parent window container's [Configuration]
    +     * @param windowLayoutInfo The parent window container's [WindowLayoutInfo]
    +     */
    +    @VisibleForTesting
    +    internal fun calculateOverlayAttributes(
    +        tag: String,
    +        initialOverlayAttrs: OverlayAttributes?,
    +        windowMetrics: WindowMetrics,
    +        configuration: Configuration,
    +        windowLayoutInfo: WindowLayoutInfo,
    +    ): OverlayAttributes {
    +        val defaultOverlayAttrs = getUpdatedOverlayAttributes(tag)
    +            ?: initialOverlayAttrs
    +            ?: throw IllegalArgumentException(
    +                "Can't retrieve overlay attributes from launch options"
    +            )
    +        val currentOverlayAttrs = overlayAttributesCalculator?.invoke(
    +            OverlayAttributesCalculatorParams(
    +                windowMetrics,
    +                configuration,
    +                windowLayoutInfo,
    +                tag,
    +                defaultOverlayAttrs,
    +            )
    +        ) ?: defaultOverlayAttrs
    +
    +        overlayTagToCurrentAttributesMap[tag] = currentOverlayAttrs
    +
    +        return currentOverlayAttrs
    +    }
    +
    +    @VisibleForTesting
    +    internal open fun getUpdatedOverlayAttributes(overlayTag: String): OverlayAttributes? =
    +        overlayTagToDefaultAttributesMap[overlayTag]
    +
    +    internal open fun updateOverlayAttributes(
    +        overlayTag: String,
    +        overlayAttributes: OverlayAttributes
    +    ) {
    +        globalLock.withLock {
    +            val activityStackToken = overlayTagToContainerMap[overlayTag]?.activityStackToken
    +                // Users may call this API before any callback coming. Try to ask platform if
    +                // this container exists.
    +                ?: embeddingExtension.getActivityStackToken(overlayTag)
    +                // Early return if there's no such ActivityStack associated with the tag.
    +                ?: return
    +
    +            embeddingExtension.updateActivityStackAttributes(
    +                activityStackToken,
    +                overlayAttributes.toActivityStackAttributes(
    +                    embeddingExtension.getParentContainerInfo(activityStackToken)!!
    +                )
    +            )
    +
    +            // Update the tag-overlayAttributes map, which will be treated as the default
    +            // overlayAttributes in calculator.
    +            overlayTagToDefaultAttributesMap[overlayTag] = overlayAttributes
    +            overlayTagToCurrentAttributesMap[overlayTag] = overlayAttributes
    +        }
    +    }
    +
    +    private fun OverlayAttributes.toActivityStackAttributes(
    +        parentContainerInfo: ParentContainerInfo
    +    ): ActivityStackAttributes = ActivityStackAttributes.Builder()
    +        .setRelativeBounds(
    +            EmbeddingBounds.translateEmbeddingBounds(
    +                bounds,
    +                adapter.translate(parentContainerInfo)
    +            ).toRect()
    +        ).setWindowAttributes(adapter.translateWindowAttributes())
    +        .build()
    +
    +    private fun List.getOverlayContainers(): List =
    +        filter { activityStack -> activityStack.tag != null }.toList()
    +
    +    open fun addOverlayInfoCallback(
    +        overlayTag: String,
    +        executor: Executor,
    +        overlayInfoCallback: JetpackConsumer,
    +    ) {
    +        globalLock.withLock {
    +            val callback = Consumer> { activityStacks ->
    +                val overlayInfoList = activityStacks.filter { activityStack ->
    +                    activityStack.tag == overlayTag
    +                }
    +                if (overlayInfoList.size > 1) {
    +                    throw IllegalStateException(
    +                        "There must be at most one overlay ActivityStack with $overlayTag"
    +                    )
    +                }
    +                val overlayInfo = if (overlayInfoList.isEmpty()) {
    +                    OverlayInfo(
    +                        overlayTag,
    +                        currentOverlayAttributes = null,
    +                        activityStack = null,
    +                    )
    +                } else {
    +                    overlayInfoList.first().toOverlayInfo()
    +                }
    +                overlayInfoCallback.accept(overlayInfo)
    +            }
    +            overlayInfoToActivityStackCallbackMap[overlayInfoCallback] = callback
    +
    +            embeddingExtension.registerActivityStackCallback(executor, callback)
    +        }
    +    }
    +
    +    private fun ActivityStack.toOverlayInfo(): OverlayInfo = OverlayInfo(
    +        tag!!,
    +        overlayTagToCurrentAttributesMap[tag!!],
    +        adapter.translate(this),
    +    )
    +
    +    open fun removeOverlayInfoCallback(overlayInfoCallback: JetpackConsumer) {
    +        globalLock.withLock {
    +            val callback = overlayInfoToActivityStackCallbackMap.remove(overlayInfoCallback)
    +            if (callback != null) {
    +                embeddingExtension.unregisterActivityStackCallback(callback)
    +            }
    +        }
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt b/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt
    new file mode 100644
    index 0000000..1d151c1
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/OverlayCreateParams.kt
    
    @@ -0,0 +1,90 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import java.util.UUID
    +
    +/**
    + * The parameter container to create an overlay [ActivityStack].
    + *
    + * If there's an shown overlay [ActivityStack] associated with the [tag], the existing
    + * [ActivityStack] will be:
    + * - Dismissed if the overlay [ActivityStack] is in different task from the launched one
    + * - Updated with [OverlayAttributes] if the overlay [ActivityStack] is in the same task.
    + *
    + * See [android.os.Bundle.setOverlayCreateParams] for how to create an overlay [ActivityStack].
    + *
    + * @property tag The unique identifier of the overlay [ActivityStack], which will be generated
    + * automatically if not specified.
    + * @property overlayAttributes The attributes of the overlay [ActivityStack], which defaults to
    + * the default value of [OverlayAttributes].
    + * @constructor creates a parameter container to launch an overlay [ActivityStack].
    + */
    +class OverlayCreateParams @JvmOverloads constructor(
    +    val tag: String = generateOverlayTag(),
    +    val overlayAttributes: OverlayAttributes = OverlayAttributes.Builder().build(),
    +) {
    +    override fun toString(): String =
    +        "${OverlayCreateParams::class.simpleName}:{ " +
    +            ", tag=$tag" +
    +            ", attrs=$overlayAttributes" +
    +            "}"
    +
    +    /**
    +     * The [OverlayCreateParams] builder.
    +     */
    +    class Builder {
    +        private var tag: String? = null
    +        private var launchAttrs: OverlayAttributes? = null
    +
    +        /**
    +         * Sets the overlay [ActivityStack]'s unique identifier. The builder will generate one
    +         * automatically if not specified.
    +         *
    +         * @param tag The unique identifier of the overlay [ActivityStack] to create.
    +         * @return The [OverlayCreateParams] builder.
    +         */
    +        fun setTag(tag: String): Builder = apply { this.tag = tag }
    +
    +        /**
    +         * Sets the overlay [ActivityStack]'s attributes, which defaults to the default value of
    +         * [OverlayAttributes.Builder].
    +         *
    +         * @param attrs The [OverlayAttributes].
    +         * @return The [OverlayCreateParams] builder.
    +         */
    +        fun setOverlayAttributes(
    +            attrs: OverlayAttributes
    +        ): Builder = apply { launchAttrs = attrs }
    +
    +        /** Builds the [OverlayCreateParams] */
    +        fun build(): OverlayCreateParams = OverlayCreateParams(
    +            tag ?: generateOverlayTag(),
    +            launchAttrs ?: OverlayAttributes.Builder().build()
    +        )
    +    }
    +
    +    companion object {
    +
    +        /**
    +         * A helper function to generate a random unique identifier.
    +         */
    +        @JvmStatic
    +        fun generateOverlayTag(): String =
    +            UUID.randomUUID().toString().substring(IntRange(0, 32))
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
    new file mode 100644
    index 0000000..8f54e36
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/OverlayInfo.kt
    
    @@ -0,0 +1,46 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.app.Activity
    +
    +/**
    + * Describes an overlay [ActivityStack] associated with [OverlayCreateParams.tag].
    + */
    +class OverlayInfo internal constructor(
    +    /** The unique identifier associated with the overlay [ActivityStack]. */
    +    val overlayTag: String,
    +    /**
    +     * The [OverlayAttributes] of the overlay [ActivityStack] if it exists, or `null` if there's no
    +     * such a [ActivityStack]
    +     */
    +    val currentOverlayAttributes: OverlayAttributes?,
    +    /**
    +     * The overlay [ActivityStack] associated with [overlayTag], or `null` if there's no such a
    +     * [ActivityStack].
    +     */
    +    val activityStack: ActivityStack?
    +) {
    +    operator fun contains(activity: Activity): Boolean = activityStack?.contains(activity) ?: false
    +
    +    override fun toString(): String =
    +        "${OverlayInfo::class.java.simpleName}: {" +
    +            "tag=$overlayTag" +
    +            ", currentOverlayAttrs=$currentOverlayAttributes" +
    +            ", activityStack=$activityStack" +
    +            "}"
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt b/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt
    new file mode 100644
    index 0000000..a02917a
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/ParentContainerInfo.kt
    
    @@ -0,0 +1,48 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import android.content.res.Configuration
    +import androidx.core.view.WindowInsetsCompat
    +import androidx.window.core.Bounds
    +import androidx.window.layout.WindowLayoutInfo
    +
    +/**
    + * The parent container information directly passed from WM Extensions, which is created to
    + * make test implementation easier.
    + *
    + * @property windowBounds The parent container's [Bounds].
    + * @property windowLayoutInfo The parent container's [WindowLayoutInfo].
    + * @property windowInsets The parent container's [WindowInsetsCompat].
    + * @property configuration The parent container's [Configuration].
    + * @property density The parent container's density in DP, which has the same unit as
    + * [android.util.DisplayMetrics.density].
    + */
    +internal data class ParentContainerInfo(
    +    /** The parent container's [Bounds]. */
    +    val windowBounds: Bounds,
    +    /** The parent container's [WindowLayoutInfo]. */
    +    val windowLayoutInfo: WindowLayoutInfo,
    +    /** The parent container's [WindowInsetsCompat] */
    +    val windowInsets: WindowInsetsCompat,
    +    /** The parent container's [Configuration]. */
    +    val configuration: Configuration,
    +    /**
    +     * The parent container's density in DP, which has the same unit as
    +     * [android.util.DisplayMetrics.density].
    +     */
    +    val density: Float,
    +)
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
    index e2cb653..acc5668 100644
    --- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
    
    @@ -174,12 +174,20 @@
                     ALWAYS.value
                 )
                 val clearTop = typedArray.getBoolean(R.styleable.SplitPairRule_clearTop, false)
    +            val animationBackgroundColor = typedArray.getColor(
    +                R.styleable.SplitPairRule_animationBackgroundColor,
    +                0
    +            )
    +            typedArray.recycle()
     
                 val defaultAttrs = SplitAttributes.Builder()
                     .setSplitType(SplitAttributes.SplitType.buildSplitTypeFromValue(ratio))
                     .setLayoutDirection(
                         SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
                     )
    +                .setAnimationBackground(
    +                    EmbeddingAnimationBackground.buildFromValue(animationBackgroundColor)
    +                )
                     .build()
     
                 SplitPairRule.Builder(emptySet())
    @@ -251,12 +259,20 @@
                     R.styleable.SplitPlaceholderRule_splitLayoutDirection,
                     LOCALE.value
                 )
    +            val animationBackgroundColor = typedArray.getColor(
    +                R.styleable.SplitPlaceholderRule_animationBackgroundColor,
    +                0
    +            )
    +            typedArray.recycle()
     
                 val defaultAttrs = SplitAttributes.Builder()
                     .setSplitType(SplitAttributes.SplitType.buildSplitTypeFromValue(ratio))
                     .setLayoutDirection(
                         SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
                     )
    +                .setAnimationBackground(
    +                    EmbeddingAnimationBackground.buildFromValue(animationBackgroundColor)
    +                )
                     .build()
                 val packageName = context.applicationContext.packageName
                 val placeholderActivityClassName = buildClassName(
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
    index c13634f..0c89e53 100644
    --- a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
    
    @@ -18,25 +18,42 @@
     
     import android.app.Activity
     import android.content.Intent
    +import android.content.res.Configuration
    +import android.graphics.Rect
    +import android.os.Bundle
    +import android.os.IBinder
    +import android.view.WindowMetrics
     import androidx.annotation.VisibleForTesting
     import androidx.window.SafeWindowExtensionsProvider
    +import androidx.window.WindowSdkExtensions
     import androidx.window.core.ConsumerAdapter
    -import androidx.window.core.ExtensionsUtil
     import androidx.window.extensions.WindowExtensions
     import androidx.window.extensions.core.util.function.Consumer
     import androidx.window.extensions.core.util.function.Function
    +import androidx.window.extensions.core.util.function.Predicate;
     import androidx.window.extensions.embedding.ActivityEmbeddingComponent
     import androidx.window.extensions.embedding.ActivityRule
     import androidx.window.extensions.embedding.ActivityStack
    +import androidx.window.extensions.embedding.ActivityStackAttributes
    +import androidx.window.extensions.embedding.ActivityStackAttributesCalculatorParams
    +import androidx.window.extensions.embedding.AnimationBackground
    +import androidx.window.extensions.embedding.EmbeddedActivityWindowInfo
    +import androidx.window.extensions.embedding.ParentContainerInfo
     import androidx.window.extensions.embedding.SplitAttributes
     import androidx.window.extensions.embedding.SplitAttributes.SplitType
    +import androidx.window.extensions.embedding.SplitAttributesCalculatorParams
     import androidx.window.extensions.embedding.SplitInfo
     import androidx.window.extensions.embedding.SplitPairRule
    +import androidx.window.extensions.embedding.SplitPinRule
     import androidx.window.extensions.embedding.SplitPlaceholderRule
    +import androidx.window.extensions.embedding.SplitRule
    +import androidx.window.extensions.embedding.WindowAttributes
    +import androidx.window.extensions.layout.WindowLayoutInfo
     import androidx.window.reflection.ReflectionUtils.doesReturn
     import androidx.window.reflection.ReflectionUtils.isPublic
     import androidx.window.reflection.ReflectionUtils.validateReflection
     import androidx.window.reflection.WindowExtensionsConstants.ACTIVITY_EMBEDDING_COMPONENT_CLASS
    +import java.util.concurrent.Executor
     
     /**
      * Reflection Guard for [ActivityEmbeddingComponent].
    @@ -69,10 +86,12 @@
             }
             // TODO(b/267573854) : update logic to fallback to lower version
             //  if higher version is not matched
    -        return when (ExtensionsUtil.safeVendorApiLevel) {
    +        return when (WindowSdkExtensions.getInstance().extensionVersion) {
                 1 -> hasValidVendorApiLevel1()
    -            in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
    -            // TODO(b/267956499) : add  hasValidVendorApiLevel3
    +            2 -> hasValidVendorApiLevel2()
    +            in 3..4 -> hasValidVendorApiLevel3() // No additional API in 4.
    +            5 -> hasValidVendorApiLevel5()
    +            in 6..Int.MAX_VALUE -> hasValidVendorApiLevel6()
                 else -> false
             }
         }
    @@ -83,25 +102,35 @@
                 isActivityEmbeddingComponentValid()
     
         /**
    -     * [WindowExtensions.VENDOR_API_LEVEL_1] includes the following methods:
    +     * Vendor API level 1 includes the following methods:
          *  - [ActivityEmbeddingComponent.setEmbeddingRules]
          *  - [ActivityEmbeddingComponent.isActivityEmbedded]
          *  - [ActivityEmbeddingComponent.setSplitInfoCallback] with [java.util.function.Consumer]
    +     *  - [SplitRule.getSplitRatio]
    +     *  - [SplitRule.getLayoutDirection]
          * and following classes:
          *  - [ActivityRule]
    +     *  - [ActivityRule.Builder]
          *  - [SplitInfo]
          *  - [SplitPairRule]
    +     *  - [SplitPairRule.Builder]
          *  - [SplitPlaceholderRule]
    +     *  - [SplitPlaceholderRule.Builder]
          */
         @VisibleForTesting
         internal fun hasValidVendorApiLevel1(): Boolean {
             return isMethodSetEmbeddingRulesValid() &&
                 isMethodIsActivityEmbeddedValid() &&
                 isMethodSetSplitInfoCallbackJavaConsumerValid() &&
    +            isMethodGetSplitRatioValid() &&
    +            isMethodGetLayoutDirectionValid() &&
                 isClassActivityRuleValid() &&
    +            isClassActivityRuleBuilderLevel1Valid() &&
                 isClassSplitInfoValid() &&
                 isClassSplitPairRuleValid() &&
    -            isClassSplitPlaceholderRuleValid()
    +            isClassSplitPairRuleBuilderLevel1Valid() &&
    +            isClassSplitPlaceholderRuleValid() &&
    +            isClassSplitPlaceholderRuleBuilderLevel1Valid()
         }
     
         /**
    @@ -111,9 +140,16 @@
          *  - [ActivityEmbeddingComponent.setSplitAttributesCalculator]
          *  - [ActivityEmbeddingComponent.clearSplitAttributesCalculator]
          *  - [SplitInfo.getSplitAttributes]
    +     *  - [SplitPlaceholderRule.getFinishPrimaryWithPlaceholder]
    +     *  - [SplitRule.getDefaultSplitAttributes]
          * and following classes:
    +     *  - [ActivityRule.Builder]
    +     *  - [EmbeddingRule]
          *  - [SplitAttributes]
          *  - [SplitAttributes.SplitType]
    +     *  - [SplitAttributesCalculatorParams]
    +     *  - [SplitPairRule.Builder]
    +     *  - [SplitPlaceholderRule.Builder]
          */
         @VisibleForTesting
         internal fun hasValidVendorApiLevel2(): Boolean {
    @@ -122,10 +158,125 @@
                 isMethodClearSplitInfoCallbackValid() &&
                 isMethodSplitAttributesCalculatorValid() &&
                 isMethodGetSplitAttributesValid() &&
    +            isMethodGetFinishPrimaryWithPlaceholderValid() &&
    +            isMethodGetDefaultSplitAttributesValid() &&
    +            isClassActivityRuleBuilderLevel2Valid() &&
    +            isClassEmbeddingRuleValid() &&
                 isClassSplitAttributesValid() &&
    -            isClassSplitTypeValid()
    +            isClassSplitAttributesCalculatorParamsValid() &&
    +            isClassSplitTypeValid() &&
    +            isClassSplitPairRuleBuilderLevel2Valid() &&
    +            isClassSplitPlaceholderRuleBuilderLevel2Valid()
         }
     
    +    /**
    +     * Vendor API level 3 includes the following methods:
    +     * - [ActivityEmbeddingComponent.updateSplitAttributes]
    +     * - [ActivityEmbeddingComponent.invalidateTopVisibleSplitAttributes]
    +     * - [SplitInfo.getToken]
    +     */
    +    @VisibleForTesting
    +    internal fun hasValidVendorApiLevel3(): Boolean =
    +        hasValidVendorApiLevel2() &&
    +            isMethodInvalidateTopVisibleSplitAttributesValid() &&
    +            isMethodUpdateSplitAttributesValid() &&
    +            isMethodSplitInfoGetTokenValid()
    +
    +    /**
    +     * Vendor API level 5 includes the following methods:
    +     * - [ActivityEmbeddingComponent.registerActivityStackCallback]
    +     * - [ActivityEmbeddingComponent.unregisterActivityStackCallback]
    +     * - [ActivityStack.getActivityStackToken]
    +     * - [ActivityStack.Token.createFromBinder]
    +     * - [ActivityStack.Token.readFromBundle]
    +     * - [ActivityStack.Token.toBundle]
    +     * - [ActivityStack.Token.INVALID_ACTIVITY_STACK_TOKEN]
    +     * - [AnimationBackground.createColorBackground]
    +     * - [AnimationBackground.ANIMATION_BACKGROUND_DEFAULT]
    +     * - [SplitAttributes.getAnimationBackground]
    +     * - [SplitAttributes.Builder.setAnimationBackground]
    +     * - [WindowAttributes.getDimAreaBehavior]
    +     * - [SplitAttributes.getWindowAttributes]
    +     * - [SplitAttributes.Builder.setWindowAttributes]
    +     * - [SplitPinRule.isSticky]
    +     * - [ActivityEmbeddingComponent.pinTopActivityStack]
    +     * - [ActivityEmbeddingComponent.unpinTopActivityStack]
    +     * - [ActivityEmbeddingComponent.updateSplitAttributes] with [SplitInfo.Token]
    +     * - [SplitInfo.getSplitInfoToken]
    +     * and following classes:
    +     * - [AnimationBackground]
    +     * - [ActivityStack.Token]
    +     * - [WindowAttributes]
    +     * - [SplitInfo.Token]
    +     */
    +    @VisibleForTesting
    +    internal fun hasValidVendorApiLevel5(): Boolean =
    +        hasValidVendorApiLevel3() &&
    +            isActivityStackGetActivityStackTokenValid() &&
    +            isMethodRegisterActivityStackCallbackValid() &&
    +            isMethodUnregisterActivityStackCallbackValid() &&
    +            isMethodPinUnpinTopActivityStackValid() &&
    +            isMethodUpdateSplitAttributesWithTokenValid() &&
    +            isMethodGetSplitInfoTokenValid() &&
    +            isClassAnimationBackgroundValid() &&
    +            isClassActivityStackTokenValid() &&
    +            isClassWindowAttributesValid() &&
    +            isClassSplitInfoTokenValid()
    +
    +    /**
    +     * Vendor API level 6 includes the following methods:
    +     * - [ActivityEmbeddingComponent.clearActivityStackAttributesCalculator]
    +     * - [ActivityEmbeddingComponent.clearEmbeddedActivityWindowInfoCallback]
    +     * - [ActivityEmbeddingComponent.getActivityStackToken]
    +     * - [ActivityEmbeddingComponent.getEmbeddedActivityWindowInfo]
    +     * - [ActivityEmbeddingComponent.getParentContainerInfo]
    +     * - [ActivityEmbeddingComponent.setActivityStackAttributesCalculator]
    +     * - [ActivityEmbeddingComponent.setEmbeddedActivityWindowInfoCallback]
    +     * - [ActivityEmbeddingComponent.updateActivityStackAttributes]
    +     * - [ActivityStack.getTag]
    +     * and following classes:
    +     * - [ParentContainerInfo]
    +     * - [EmbeddedActivityWindowInfo]
    +     * - [ActivityStackAttributes]
    +     * - [ActivityStackAttributes.Builder]
    +     * - [ActivityStackAttributesCalculatorParams]
    +     */
    +    @VisibleForTesting
    +    internal fun hasValidVendorApiLevel6(): Boolean =
    +        hasValidVendorApiLevel5() &&
    +            isActivityStackGetTagValid() &&
    +            isMethodGetActivityStackTokenValid() &&
    +            isMethodGetParentContainerInfoValid() &&
    +            isMethodSetActivityStackAttributesCalculatorValid() &&
    +            isMethodClearActivityStackAttributesCalculatorValid() &&
    +            isMethodUpdateActivityStackAttributesValid() &&
    +            isMethodGetEmbeddedActivityWindowInfoValid() &&
    +            isMethodSetEmbeddedActivityWindowInfoCallbackValid() &&
    +            isMethodClearEmbeddedActivityWindowInfoCallbackValid() &&
    +            isClassParentContainerInfoValid() &&
    +            isClassEmbeddedActivityWindowInfoValid() &&
    +            isClassActivityStackAttributesValid() &&
    +            isClassActivityStackAttributesBuilderValid() &&
    +            isClassActivityStackAttributesCalculatorParamsValid()
    +
    +    private val activityEmbeddingComponentClass: Class<*>
    +        get() {
    +            return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
    +        }
    +
    +    private fun isActivityEmbeddingComponentValid(): Boolean {
    +        return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
    +            val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
    +            val getActivityEmbeddingComponentMethod =
    +                extensionsClass.getMethod("getActivityEmbeddingComponent")
    +            val activityEmbeddingComponentClass = activityEmbeddingComponentClass
    +            getActivityEmbeddingComponentMethod.isPublic &&
    +                getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
    +        }
    +    }
    +
    +    /** Vendor API level 1 validation methods */
    +
         private fun isMethodSetEmbeddingRulesValid(): Boolean {
             return validateReflection("ActivityEmbeddingComponent#setEmbeddingRules is not valid") {
                 val setEmbeddingRulesMethod = activityEmbeddingComponentClass.getMethod(
    @@ -147,6 +298,158 @@
             }
         }
     
    +    private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
    +        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
    +            val consumerClass =
    +                consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
    +            val setSplitInfoCallbackMethod =
    +                activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
    +            setSplitInfoCallbackMethod.isPublic
    +        }
    +    }
    +
    +    private fun isMethodGetSplitRatioValid(): Boolean =
    +        validateReflection("SplitRule#getSplitRatio is not valid") {
    +            val splitRuleClass = SplitRule::class.java
    +            val getSplitRatioMethod = splitRuleClass.getMethod("getSplitRatio")
    +            getSplitRatioMethod.isPublic &&
    +                getSplitRatioMethod.doesReturn(Float::class.java)
    +        }
    +
    +    private fun isMethodGetLayoutDirectionValid(): Boolean =
    +        validateReflection("SplitRule#getLayoutDirection is not valid") {
    +            val splitRuleClass = SplitRule::class.java
    +            val getLayoutDirectionMethod = splitRuleClass.getMethod("getLayoutDirection")
    +            getLayoutDirectionMethod.isPublic &&
    +                getLayoutDirectionMethod.doesReturn(Int::class.java)
    +        }
    +
    +    private fun isClassActivityRuleValid(): Boolean =
    +        validateReflection("Class ActivityRule is not valid") {
    +            val activityRuleClass = ActivityRule::class.java
    +            val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
    +            shouldAlwaysExpandMethod.isPublic &&
    +                shouldAlwaysExpandMethod.doesReturn(Boolean::class.java)
    +        }
    +
    +    private fun isClassActivityRuleBuilderLevel1Valid(): Boolean =
    +        validateReflection("Class ActivityRule.Builder is not valid") {
    +            val activityRuleBuilderClass = ActivityRule.Builder::class.java
    +            val setShouldAlwaysExpandMethod = activityRuleBuilderClass.getMethod(
    +                "setShouldAlwaysExpand",
    +                Boolean::class.java
    +            )
    +            setShouldAlwaysExpandMethod.isPublic &&
    +                setShouldAlwaysExpandMethod.doesReturn(ActivityRule.Builder::class.java)
    +        }
    +
    +    private fun isClassSplitInfoValid(): Boolean =
    +        validateReflection("Class SplitInfo is not valid") {
    +            val splitInfoClass = SplitInfo::class.java
    +            val getPrimaryActivityStackMethod =
    +                splitInfoClass.getMethod("getPrimaryActivityStack")
    +            val getSecondaryActivityStackMethod =
    +                splitInfoClass.getMethod("getSecondaryActivityStack")
    +            val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
    +            getPrimaryActivityStackMethod.isPublic &&
    +                getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
    +                getSecondaryActivityStackMethod.isPublic &&
    +                getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
    +                getSplitRatioMethod.isPublic &&
    +                getSplitRatioMethod.doesReturn(Float::class.java)
    +        }
    +
    +    private fun isClassSplitPairRuleValid(): Boolean =
    +        validateReflection("Class SplitPairRule is not valid") {
    +            val splitPairRuleClass = SplitPairRule::class.java
    +            val getFinishPrimaryWithSecondaryMethod =
    +                splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
    +            val getFinishSecondaryWithPrimaryMethod =
    +                splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
    +            val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
    +            getFinishPrimaryWithSecondaryMethod.isPublic &&
    +                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
    +                getFinishSecondaryWithPrimaryMethod.isPublic &&
    +                getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
    +                shouldClearTopMethod.isPublic &&
    +                shouldClearTopMethod.doesReturn(Boolean::class.java)
    +        }
    +
    +    private fun isClassSplitPairRuleBuilderLevel1Valid(): Boolean =
    +        validateReflection("Class SplitPairRule.Builder is not valid") {
    +            val splitPairRuleBuilderClass = SplitPairRule.Builder::class.java
    +            val setSplitRatioMethod = splitPairRuleBuilderClass.getMethod(
    +                "setSplitRatio",
    +                Float::class.java
    +            )
    +            val setLayoutDirectionMethod = splitPairRuleBuilderClass.getMethod(
    +                "setLayoutDirection",
    +                Int::class.java
    +            )
    +            setSplitRatioMethod.isPublic &&
    +                setSplitRatioMethod.doesReturn(SplitPairRule.Builder::class.java) &&
    +                setLayoutDirectionMethod.isPublic &&
    +                setLayoutDirectionMethod.doesReturn(SplitPairRule.Builder::class.java)
    +        }
    +
    +    private fun isClassSplitPlaceholderRuleValid(): Boolean =
    +        validateReflection("Class SplitPlaceholderRule is not valid") {
    +            val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
    +            val getPlaceholderIntentMethod =
    +                splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
    +            val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
    +            val getFinishPrimaryWithSecondaryMethod =
    +                splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
    +            getPlaceholderIntentMethod.isPublic &&
    +                getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
    +                isStickyMethod.isPublic &&
    +                isStickyMethod.doesReturn(Boolean::class.java) &&
    +                getFinishPrimaryWithSecondaryMethod.isPublic &&
    +                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
    +        }
    +
    +    private fun isClassSplitPlaceholderRuleBuilderLevel1Valid(): Boolean =
    +        validateReflection("Class SplitPlaceholderRule.Builder is not valid") {
    +            val splitPlaceholderRuleBuilderClass = SplitPlaceholderRule.Builder::class.java
    +            val setSplitRatioMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setSplitRatio",
    +                Float::class.java
    +            )
    +            val setLayoutDirectionMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setLayoutDirection",
    +                Int::class.java
    +            )
    +            val setStickyMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setSticky",
    +                Boolean::class.java
    +            )
    +            val setFinishPrimaryWithSecondaryMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setFinishPrimaryWithSecondary",
    +                Int::class.java
    +            )
    +            setSplitRatioMethod.isPublic &&
    +                setSplitRatioMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
    +                setLayoutDirectionMethod.isPublic &&
    +                setLayoutDirectionMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
    +                setStickyMethod.isPublic &&
    +                setStickyMethod.doesReturn(SplitPlaceholderRule.Builder::class.java) &&
    +                setFinishPrimaryWithSecondaryMethod.isPublic &&
    +                setFinishPrimaryWithSecondaryMethod.doesReturn(
    +                    SplitPlaceholderRule.Builder::class.java)
    +        }
    +
    +    /** Vendor API level 2 validation methods */
    +
    +    private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
    +        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
    +            val setSplitInfoCallbackMethod = activityEmbeddingComponentClass.getMethod(
    +                "setSplitInfoCallback",
    +                Consumer::class.java
    +            )
    +            setSplitInfoCallbackMethod.isPublic
    +        }
    +    }
    +
         private fun isMethodClearSplitInfoCallbackValid(): Boolean {
             return validateReflection(
                 "ActivityEmbeddingComponent#clearSplitInfoCallback is not valid"
    @@ -180,6 +483,48 @@
                     getSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
             }
     
    +    private fun isMethodGetFinishPrimaryWithPlaceholderValid(): Boolean =
    +        validateReflection("SplitPlaceholderRule#getFinishPrimaryWithPlaceholder is not valid") {
    +            val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
    +            val getFinishPrimaryWithPlaceholderMethod =
    +                splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithPlaceholder")
    +            getFinishPrimaryWithPlaceholderMethod.isPublic &&
    +                getFinishPrimaryWithPlaceholderMethod.doesReturn(Int::class.java)
    +        }
    +
    +    private fun isMethodGetDefaultSplitAttributesValid(): Boolean =
    +        validateReflection("SplitRule#getDefaultSplitAttributes is not valid") {
    +            val splitRuleClass = SplitRule::class.java
    +            val getDefaultSplitAttributesMethod =
    +                splitRuleClass.getMethod("getDefaultSplitAttributes")
    +            getDefaultSplitAttributesMethod.isPublic &&
    +                getDefaultSplitAttributesMethod.doesReturn(SplitAttributes::class.java)
    +        }
    +
    +    private fun isClassActivityRuleBuilderLevel2Valid(): Boolean =
    +        validateReflection("Class ActivityRule.Builder is not valid") {
    +            val activityRuleBuilderClass = ActivityRule.Builder::class.java
    +            val activityRuleBuilderConstructor =
    +                activityRuleBuilderClass.getDeclaredConstructor(
    +                    Predicate::class.java,
    +                    Predicate::class.java)
    +            val setTagMethod = activityRuleBuilderClass.getMethod(
    +                "setTag",
    +                String::class.java
    +            )
    +            activityRuleBuilderConstructor.isPublic &&
    +                setTagMethod.isPublic &&
    +                setTagMethod.doesReturn(ActivityRule.Builder::class.java)
    +        }
    +
    +    private fun isClassEmbeddingRuleValid(): Boolean =
    +        validateReflection("Class EmbeddingRule is not valid") {
    +            val embeddingRuleClass = EmbeddingRule::class.java
    +            val getTagMethod = embeddingRuleClass.getMethod("getTag")
    +            getTagMethod.isPublic &&
    +                getTagMethod.doesReturn(String::class.java)
    +        }
    +
         private fun isClassSplitAttributesValid(): Boolean =
             validateReflection("Class SplitAttributes is not valid") {
                 val splitAttributesClass = SplitAttributes::class.java
    @@ -202,6 +547,36 @@
                     setSplitTypeMethod.isPublic && setLayoutDirectionMethod.isPublic
             }
     
    +    @Suppress("newApi") // Suppress lint check for WindowMetrics
    +    private fun isClassSplitAttributesCalculatorParamsValid(): Boolean =
    +        validateReflection("Class SplitAttributesCalculatorParams is not valid") {
    +            val splitAttributesCalculatorParamsClass = SplitAttributesCalculatorParams::class.java
    +            val getParentWindowMetricsMethod =
    +                splitAttributesCalculatorParamsClass.getMethod("getParentWindowMetrics")
    +            val getParentConfigurationMethod =
    +                splitAttributesCalculatorParamsClass.getMethod("getParentConfiguration")
    +            val getDefaultSplitAttributesMethod =
    +                splitAttributesCalculatorParamsClass.getMethod("getDefaultSplitAttributes")
    +            val areDefaultConstraintsSatisfiedMethod =
    +                splitAttributesCalculatorParamsClass.getMethod("areDefaultConstraintsSatisfied")
    +            val getParentWindowLayoutInfoMethod =
    +                splitAttributesCalculatorParamsClass.getMethod("getParentWindowLayoutInfo")
    +            val getSplitRuleTagMethod =
    +                splitAttributesCalculatorParamsClass.getMethod("getSplitRuleTag")
    +            getParentWindowMetricsMethod.isPublic &&
    +                getParentWindowMetricsMethod.doesReturn(WindowMetrics::class.java) &&
    +                getParentConfigurationMethod.isPublic &&
    +                getParentConfigurationMethod.doesReturn(Configuration::class.java) &&
    +                getDefaultSplitAttributesMethod.isPublic &&
    +                getDefaultSplitAttributesMethod.doesReturn(SplitAttributes::class.java) &&
    +                areDefaultConstraintsSatisfiedMethod.isPublic &&
    +                areDefaultConstraintsSatisfiedMethod.doesReturn(Boolean::class.java) &&
    +                getParentWindowLayoutInfoMethod.isPublic &&
    +                getParentWindowLayoutInfoMethod.doesReturn(WindowLayoutInfo::class.java) &&
    +                getSplitRuleTagMethod.isPublic &&
    +                getSplitRuleTagMethod.doesReturn(String::class.java)
    +        }
    +
         private fun isClassSplitTypeValid(): Boolean =
             validateReflection("Class SplitAttributes.SplitType is not valid") {
                 val ratioSplitTypeClass = SplitType.RatioSplitType::class.java
    @@ -228,101 +603,433 @@
                     expandContainersSplitTypeConstructor.isPublic
             }
     
    -    private fun isMethodSetSplitInfoCallbackJavaConsumerValid(): Boolean {
    -        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
    -            val consumerClass =
    -                consumerAdapter.consumerClassOrNull() ?: return@validateReflection false
    -            val setSplitInfoCallbackMethod =
    -                activityEmbeddingComponentClass.getMethod("setSplitInfoCallback", consumerClass)
    -            setSplitInfoCallbackMethod.isPublic
    -        }
    -    }
    -
    -    private fun isClassActivityRuleValid(): Boolean =
    -        validateReflection("Class ActivityRule is not valid") {
    -            val activityRuleClass = ActivityRule::class.java
    -            val shouldAlwaysExpandMethod = activityRuleClass.getMethod("shouldAlwaysExpand")
    -            val activityRuleBuilderClass = ActivityRule.Builder::class.java
    -            val setShouldAlwaysExpandMethod = activityRuleBuilderClass.getMethod(
    -                "setShouldAlwaysExpand",
    -                Boolean::class.java
    +    private fun isClassSplitPairRuleBuilderLevel2Valid(): Boolean =
    +        validateReflection("Class SplitPairRule.Builder is not valid") {
    +            val splitPairRuleBuilderClass = SplitPairRule.Builder::class.java
    +            val splitPairRuleBuilderConstructor =
    +                splitPairRuleBuilderClass.getDeclaredConstructor(
    +                    Predicate::class.java,
    +                    Predicate::class.java,
    +                    Predicate::class.java)
    +            val setDefaultSplitAttributesMethod = splitPairRuleBuilderClass.getMethod(
    +                "setDefaultSplitAttributes",
    +                SplitAttributes::class.java,
                 )
    -            shouldAlwaysExpandMethod.isPublic &&
    -                shouldAlwaysExpandMethod.doesReturn(Boolean::class.java) &&
    -                setShouldAlwaysExpandMethod.isPublic
    +            val setTagMethod = splitPairRuleBuilderClass.getMethod(
    +                "setTag",
    +                String::class.java
    +            )
    +            splitPairRuleBuilderConstructor.isPublic &&
    +                setDefaultSplitAttributesMethod.isPublic &&
    +                setDefaultSplitAttributesMethod.doesReturn(SplitPairRule.Builder::class.java) &&
    +                setTagMethod.isPublic &&
    +                setTagMethod.doesReturn(SplitPairRule.Builder::class.java)
             }
     
    -    private fun isClassSplitInfoValid(): Boolean =
    -        validateReflection("Class SplitInfo is not valid") {
    +    private fun isClassSplitPlaceholderRuleBuilderLevel2Valid(): Boolean =
    +        validateReflection("Class SplitPlaceholderRule.Builder is not valid") {
    +            val splitPlaceholderRuleBuilderClass = SplitPlaceholderRule.Builder::class.java
    +            val splitPlaceholderRuleBuilderConstructor =
    +                splitPlaceholderRuleBuilderClass.getDeclaredConstructor(
    +                    Intent::class.java,
    +                    Predicate::class.java,
    +                    Predicate::class.java,
    +                    Predicate::class.java)
    +            val setDefaultSplitAttributesMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setDefaultSplitAttributes",
    +                SplitAttributes::class.java,
    +            )
    +            val setFinishPrimaryWithPlaceholderMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setFinishPrimaryWithPlaceholder",
    +                Int::class.java
    +            )
    +            val setTagMethod = splitPlaceholderRuleBuilderClass.getMethod(
    +                "setTag",
    +                String::class.java
    +            )
    +            splitPlaceholderRuleBuilderConstructor.isPublic &&
    +                setDefaultSplitAttributesMethod.isPublic &&
    +                setDefaultSplitAttributesMethod.doesReturn(
    +                    SplitPlaceholderRule.Builder::class.java) &&
    +                setFinishPrimaryWithPlaceholderMethod.isPublic &&
    +                setFinishPrimaryWithPlaceholderMethod.doesReturn(
    +                    SplitPlaceholderRule.Builder::class.java) &&
    +                setTagMethod.isPublic &&
    +                setTagMethod.doesReturn(SplitPlaceholderRule.Builder::class.java)
    +        }
    +
    +    /** Vendor API level 3 validation methods */
    +
    +    private fun isMethodInvalidateTopVisibleSplitAttributesValid(): Boolean =
    +        validateReflection("#invalidateTopVisibleSplitAttributes is not valid") {
    +            val invalidateTopVisibleSplitAttributesMethod = activityEmbeddingComponentClass
    +                .getMethod(
    +                    "invalidateTopVisibleSplitAttributes"
    +                )
    +            invalidateTopVisibleSplitAttributesMethod.isPublic
    +        }
    +
    +    private fun isMethodUpdateSplitAttributesValid(): Boolean =
    +        validateReflection("#updateSplitAttributes is not valid") {
    +            val updateSplitAttributesMethod = activityEmbeddingComponentClass.getMethod(
    +                "updateSplitAttributes",
    +                IBinder::class.java,
    +                SplitAttributes::class.java
    +            )
    +            updateSplitAttributesMethod.isPublic
    +        }
    +
    +    private fun isMethodSplitInfoGetTokenValid(): Boolean =
    +        validateReflection("SplitInfo#getToken is not valid") {
                 val splitInfoClass = SplitInfo::class.java
    -            val getPrimaryActivityStackMethod =
    -                splitInfoClass.getMethod("getPrimaryActivityStack")
    -            val getSecondaryActivityStackMethod =
    -                splitInfoClass.getMethod("getSecondaryActivityStack")
    -            val getSplitRatioMethod = splitInfoClass.getMethod("getSplitRatio")
    -            getPrimaryActivityStackMethod.isPublic &&
    -                getPrimaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
    -                getSecondaryActivityStackMethod.isPublic &&
    -                getSecondaryActivityStackMethod.doesReturn(ActivityStack::class.java) &&
    -                getSplitRatioMethod.isPublic &&
    -                getSplitRatioMethod.doesReturn(Float::class.java)
    +            val getTokenMethod = splitInfoClass.getMethod("getToken")
    +            getTokenMethod.isPublic &&
    +                getTokenMethod.doesReturn(IBinder::class.java)
             }
     
    -    private fun isClassSplitPairRuleValid(): Boolean =
    -        validateReflection("Class SplitPairRule is not valid") {
    -            val splitPairRuleClass = SplitPairRule::class.java
    -            val getFinishPrimaryWithSecondaryMethod =
    -                splitPairRuleClass.getMethod("getFinishPrimaryWithSecondary")
    -            val getFinishSecondaryWithPrimaryMethod =
    -                splitPairRuleClass.getMethod("getFinishSecondaryWithPrimary")
    -            val shouldClearTopMethod = splitPairRuleClass.getMethod("shouldClearTop")
    -            getFinishPrimaryWithSecondaryMethod.isPublic &&
    -                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java) &&
    -                getFinishSecondaryWithPrimaryMethod.isPublic &&
    -                getFinishSecondaryWithPrimaryMethod.doesReturn(Int::class.java) &&
    -                shouldClearTopMethod.isPublic &&
    -                shouldClearTopMethod.doesReturn(Boolean::class.java)
    +    /** Vendor API level 5 validation methods */
    +
    +    private fun isActivityStackGetActivityStackTokenValid(): Boolean =
    +        validateReflection("ActivityStack#getActivityToken is not valid") {
    +            val activityStackClass = ActivityStack::class.java
    +            val getActivityStackTokenMethod = activityStackClass.getMethod("getActivityStackToken")
    +
    +            getActivityStackTokenMethod.isPublic &&
    +                getActivityStackTokenMethod.doesReturn(ActivityStack.Token::class.java)
             }
     
    -    private fun isClassSplitPlaceholderRuleValid(): Boolean =
    -        validateReflection("Class SplitPlaceholderRule is not valid") {
    -            val splitPlaceholderRuleClass = SplitPlaceholderRule::class.java
    -            val getPlaceholderIntentMethod =
    -                splitPlaceholderRuleClass.getMethod("getPlaceholderIntent")
    -            val isStickyMethod = splitPlaceholderRuleClass.getMethod("isSticky")
    -            val getFinishPrimaryWithSecondaryMethod =
    -                splitPlaceholderRuleClass.getMethod("getFinishPrimaryWithSecondary")
    -            getPlaceholderIntentMethod.isPublic &&
    -                getPlaceholderIntentMethod.doesReturn(Intent::class.java) &&
    -                isStickyMethod.isPublic &&
    -                isStickyMethod.doesReturn(Boolean::class.java)
    -            getFinishPrimaryWithSecondaryMethod.isPublic &&
    -                getFinishPrimaryWithSecondaryMethod.doesReturn(Int::class.java)
    +    private fun isMethodRegisterActivityStackCallbackValid(): Boolean =
    +        validateReflection("registerActivityStackCallback is not valid") {
    +            val registerActivityStackCallbackMethod = activityEmbeddingComponentClass
    +                .getMethod(
    +                    "registerActivityStackCallback",
    +                    Executor::class.java,
    +                    Consumer::class.java
    +                )
    +            registerActivityStackCallbackMethod.isPublic
             }
     
    -    private fun isMethodSetSplitInfoCallbackWindowConsumerValid(): Boolean {
    -        return validateReflection("ActivityEmbeddingComponent#setSplitInfoCallback is not valid") {
    -            val setSplitInfoCallbackMethod = activityEmbeddingComponentClass.getMethod(
    -                "setSplitInfoCallback",
    -                Consumer::class.java
    +    private fun isMethodUnregisterActivityStackCallbackValid(): Boolean =
    +        validateReflection("unregisterActivityStackCallback is not valid") {
    +            val unregisterActivityStackCallbackMethod = activityEmbeddingComponentClass
    +                .getMethod(
    +                    "unregisterActivityStackCallback",
    +                    Consumer::class.java
    +                )
    +            unregisterActivityStackCallbackMethod.isPublic
    +        }
    +
    +    private fun isMethodPinUnpinTopActivityStackValid(): Boolean =
    +        validateReflection("#pin(unPin)TopActivityStack is not valid") {
    +            val splitPinRuleClass = SplitPinRule::class.java
    +            val isStickyMethod = splitPinRuleClass.getMethod(
    +                "isSticky"
                 )
    -            setSplitInfoCallbackMethod.isPublic
    +            val pinTopActivityStackMethod = activityEmbeddingComponentClass.getMethod(
    +                "pinTopActivityStack",
    +                Int::class.java,
    +                SplitPinRule::class.java
    +            )
    +            val unpinTopActivityStackMethod = activityEmbeddingComponentClass.getMethod(
    +                "unpinTopActivityStack",
    +                Int::class.java
    +            )
    +            isStickyMethod.isPublic &&
    +                isStickyMethod.doesReturn(Boolean::class.java) &&
    +                pinTopActivityStackMethod.isPublic &&
    +                pinTopActivityStackMethod.doesReturn(Boolean::class.java) &&
    +                unpinTopActivityStackMethod.isPublic
             }
    -    }
     
    -    private fun isActivityEmbeddingComponentValid(): Boolean {
    -        return validateReflection("WindowExtensions#getActivityEmbeddingComponent is not valid") {
    -            val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
    -            val getActivityEmbeddingComponentMethod =
    -                extensionsClass.getMethod("getActivityEmbeddingComponent")
    -            val activityEmbeddingComponentClass = activityEmbeddingComponentClass
    -            getActivityEmbeddingComponentMethod.isPublic &&
    -                getActivityEmbeddingComponentMethod.doesReturn(activityEmbeddingComponentClass)
    +    private fun isMethodUpdateSplitAttributesWithTokenValid(): Boolean =
    +        validateReflection("updateSplitAttributes is not valid") {
    +            val updateSplitAttributesMethod = activityEmbeddingComponentClass
    +                .getMethod(
    +                    "updateSplitAttributes",
    +                    SplitInfo.Token::class.java,
    +                    SplitAttributes::class.java,
    +                )
    +            updateSplitAttributesMethod.isPublic
             }
    -    }
     
    -    private val activityEmbeddingComponentClass: Class<*>
    -        get() {
    -            return loader.loadClass(ACTIVITY_EMBEDDING_COMPONENT_CLASS)
    +    private fun isMethodGetSplitInfoTokenValid(): Boolean =
    +        validateReflection("SplitInfo#getSplitInfoToken is not valid") {
    +            val splitInfoClass = SplitInfo::class.java
    +            val getSplitInfoToken = splitInfoClass.getMethod("getSplitInfoToken")
    +            getSplitInfoToken.isPublic &&
    +                getSplitInfoToken.doesReturn(SplitInfo.Token::class.java)
    +        }
    +
    +    private fun isClassAnimationBackgroundValid(): Boolean =
    +        validateReflection("Class AnimationBackground is not valid") {
    +            val animationBackgroundClass = AnimationBackground::class.java
    +            val colorBackgroundClass = AnimationBackground.ColorBackground::class.java
    +            val createColorBackgroundMethod = animationBackgroundClass.getMethod(
    +                "createColorBackground",
    +                Int::class.java
    +            )
    +            val animationBackgroundDefaultField = animationBackgroundClass.getDeclaredField(
    +                "ANIMATION_BACKGROUND_DEFAULT"
    +            )
    +            val colorBackgroundGetColor = colorBackgroundClass.getMethod(
    +                "getColor"
    +            )
    +
    +            val splitAttributesClass = SplitAttributes::class.java
    +            val getAnimationBackgroundMethod = splitAttributesClass.getMethod(
    +                "getAnimationBackground"
    +            )
    +
    +            val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
    +            val setAnimationBackgroundMethod = splitAttributesBuilderClass.getMethod(
    +                "setAnimationBackground",
    +                AnimationBackground::class.java
    +            )
    +
    +            createColorBackgroundMethod.isPublic &&
    +                createColorBackgroundMethod.doesReturn(colorBackgroundClass) &&
    +                animationBackgroundDefaultField.isPublic &&
    +                colorBackgroundGetColor.isPublic &&
    +                colorBackgroundGetColor.doesReturn(Int::class.java) &&
    +                getAnimationBackgroundMethod.isPublic &&
    +                getAnimationBackgroundMethod.doesReturn(animationBackgroundClass) &&
    +                setAnimationBackgroundMethod.isPublic &&
    +                setAnimationBackgroundMethod.doesReturn(SplitAttributes.Builder::class.java)
    +        }
    +
    +    private fun isClassActivityStackTokenValid(): Boolean =
    +        validateReflection("Class ActivityStack.Token is not valid") {
    +            val activityStackTokenClass = ActivityStack.Token::class.java
    +            val toBundleMethod = activityStackTokenClass.getMethod("toBundle")
    +            val readFromBundle = activityStackTokenClass.getMethod(
    +                "readFromBundle",
    +                Bundle::class.java
    +            )
    +            val createFromBinder = activityStackTokenClass.getMethod(
    +                "createFromBinder",
    +                IBinder::class.java
    +            )
    +            val invalidActivityStackTokenField = activityStackTokenClass.getDeclaredField(
    +                "INVALID_ACTIVITY_STACK_TOKEN"
    +            )
    +
    +            toBundleMethod.isPublic && toBundleMethod.doesReturn(Bundle::class.java) &&
    +                readFromBundle.isPublic && readFromBundle.doesReturn(activityStackTokenClass) &&
    +                createFromBinder.isPublic && createFromBinder.doesReturn(activityStackTokenClass) &&
    +                invalidActivityStackTokenField.isPublic
    +        }
    +
    +    private fun isClassWindowAttributesValid(): Boolean =
    +        validateReflection("Class WindowAttributes is not valid") {
    +            val windowAttributesClass = WindowAttributes::class.java
    +            val getDimAreaBehaviorMethod = windowAttributesClass.getMethod(
    +                "getDimAreaBehavior"
    +            )
    +
    +            val splitAttributesClass = SplitAttributes::class.java
    +            val getWindowAttributesMethod = splitAttributesClass.getMethod(
    +                "getWindowAttributes"
    +            )
    +
    +            val splitAttributesBuilderClass = SplitAttributes.Builder::class.java
    +            val setWindowAttributesMethod = splitAttributesBuilderClass.getMethod(
    +                "setWindowAttributes",
    +                WindowAttributes::class.java
    +            )
    +
    +            getDimAreaBehaviorMethod.isPublic &&
    +                getDimAreaBehaviorMethod.doesReturn(Int::class.java) &&
    +                getWindowAttributesMethod.isPublic &&
    +                getWindowAttributesMethod.doesReturn(windowAttributesClass) &&
    +                setWindowAttributesMethod.isPublic &&
    +                setWindowAttributesMethod.doesReturn(SplitAttributes.Builder::class.java)
    +        }
    +
    +    private fun isClassSplitInfoTokenValid(): Boolean =
    +        validateReflection("SplitInfo.Token is not valid") {
    +            val splitInfoTokenClass = SplitInfo.Token::class.java
    +            val createFromBinder = splitInfoTokenClass.getMethod(
    +                "createFromBinder",
    +                IBinder::class.java
    +            )
    +
    +            createFromBinder.isPublic && createFromBinder.doesReturn(splitInfoTokenClass)
    +        }
    +
    +    /** Vendor API level 6 validation methods */
    +
    +    private fun isActivityStackGetTagValid(): Boolean =
    +        validateReflection("ActivityStack#getTag is not valid") {
    +            val activityStackClass = ActivityStack::class.java
    +            val getTokenMethod = activityStackClass.getMethod("getTag")
    +
    +            getTokenMethod.isPublic && getTokenMethod.doesReturn(String::class.java)
    +        }
    +
    +    private fun isMethodGetActivityStackTokenValid(): Boolean =
    +        validateReflection("getActivityStackToken is not valid") {
    +            val getActivityStackTokenMethod = activityEmbeddingComponentClass.getMethod(
    +                "getActivityStackToken",
    +                String::class.java
    +            )
    +            getActivityStackTokenMethod.isPublic &&
    +                getActivityStackTokenMethod.doesReturn(ActivityStack.Token::class.java)
    +        }
    +
    +    private fun isMethodGetParentContainerInfoValid(): Boolean =
    +        validateReflection(
    +            "ActivityEmbeddingComponent#getParentContainerInfo is not valid") {
    +            val getParentContainerInfoMethod = activityEmbeddingComponentClass.getMethod(
    +                "getParentContainerInfo",
    +                ActivityStack.Token::class.java
    +            )
    +            getParentContainerInfoMethod.isPublic &&
    +                getParentContainerInfoMethod.doesReturn(ParentContainerInfo::class.java)
    +        }
    +
    +    private fun isMethodSetActivityStackAttributesCalculatorValid(): Boolean =
    +        validateReflection("setActivityStackAttributesCalculator is not valid") {
    +            val setActivityStackAttributesCalculatorMethod = activityEmbeddingComponentClass
    +                .getMethod("setActivityStackAttributesCalculator", Function::class.java)
    +            setActivityStackAttributesCalculatorMethod.isPublic
    +        }
    +
    +    private fun isMethodClearActivityStackAttributesCalculatorValid(): Boolean =
    +        validateReflection("clearActivityStackAttributesCalculator is not valid") {
    +            val setActivityStackAttributesCalculatorMethod = activityEmbeddingComponentClass
    +                .getMethod("clearActivityStackAttributesCalculator")
    +            setActivityStackAttributesCalculatorMethod.isPublic
    +        }
    +
    +    private fun isMethodUpdateActivityStackAttributesValid(): Boolean =
    +        validateReflection("updateActivityStackAttributes is not valid") {
    +            val updateActivityStackAttributesMethod = activityEmbeddingComponentClass
    +                .getMethod("updateActivityStackAttributes", ActivityStack.Token::class.java,
    +                    ActivityStackAttributes::class.java)
    +            updateActivityStackAttributesMethod.isPublic
    +        }
    +
    +    private fun isMethodGetEmbeddedActivityWindowInfoValid(): Boolean =
    +        validateReflection(
    +            "ActivityEmbeddingComponent#getEmbeddedActivityWindowInfo is not valid"
    +        ) {
    +            val getEmbeddedActivityWindowInfoMethod = activityEmbeddingComponentClass.getMethod(
    +                "getEmbeddedActivityWindowInfo",
    +                Activity::class.java
    +            )
    +            getEmbeddedActivityWindowInfoMethod.isPublic &&
    +                getEmbeddedActivityWindowInfoMethod.doesReturn(
    +                    EmbeddedActivityWindowInfo::class.java
    +                )
    +        }
    +
    +    private fun isMethodSetEmbeddedActivityWindowInfoCallbackValid(): Boolean =
    +        validateReflection(
    +            "ActivityEmbeddingComponent#setEmbeddedActivityWindowInfoCallback is not valid"
    +        ) {
    +            val setEmbeddedActivityWindowInfoCallbackMethod = activityEmbeddingComponentClass
    +                .getMethod(
    +                    "setEmbeddedActivityWindowInfoCallback",
    +                    Executor::class.java,
    +                    Consumer::class.java
    +                )
    +            setEmbeddedActivityWindowInfoCallbackMethod.isPublic
    +        }
    +
    +    private fun isMethodClearEmbeddedActivityWindowInfoCallbackValid(): Boolean =
    +        validateReflection(
    +            "ActivityEmbeddingComponent#clearEmbeddedActivityWindowInfoCallback is not valid"
    +        ) {
    +            val clearEmbeddedActivityWindowInfoCallbackMethod = activityEmbeddingComponentClass
    +                .getMethod(
    +                    "clearEmbeddedActivityWindowInfoCallback"
    +                )
    +            clearEmbeddedActivityWindowInfoCallbackMethod.isPublic
    +        }
    +
    +    @Suppress("newApi") // Suppress lint check for WindowMetrics
    +    private fun isClassParentContainerInfoValid(): Boolean =
    +        validateReflection("ParentContainerInfo is not valid") {
    +            val parentContainerInfoClass = ParentContainerInfo::class.java
    +            val getWindowMetricsMethod = parentContainerInfoClass.getMethod("getWindowMetrics")
    +            val getConfigurationMethod = parentContainerInfoClass.getMethod("getConfiguration")
    +            val getWindowLayoutInfoMethod = parentContainerInfoClass
    +                .getMethod("getWindowLayoutInfo")
    +            getWindowMetricsMethod.isPublic &&
    +                getWindowMetricsMethod.doesReturn(WindowMetrics::class.java) &&
    +                getConfigurationMethod.isPublic &&
    +                getConfigurationMethod.doesReturn(Configuration::class.java) &&
    +                getWindowLayoutInfoMethod.isPublic &&
    +                getWindowLayoutInfoMethod.doesReturn(WindowLayoutInfo::class.java)
    +        }
    +
    +    private fun isClassEmbeddedActivityWindowInfoValid(): Boolean =
    +        validateReflection("Class EmbeddedActivityWindowInfo is not valid") {
    +            val embeddedActivityWindowInfoClass = EmbeddedActivityWindowInfo::class.java
    +            val getActivityMethod = embeddedActivityWindowInfoClass.getMethod("getActivity")
    +            val isEmbeddedMethod = embeddedActivityWindowInfoClass.getMethod("isEmbedded")
    +            val getTaskBoundsMethod = embeddedActivityWindowInfoClass.getMethod("getTaskBounds")
    +            val getActivityStackBoundsMethod = embeddedActivityWindowInfoClass.getMethod(
    +                "getActivityStackBounds"
    +            )
    +            getActivityMethod.isPublic &&
    +                getActivityMethod.doesReturn(Activity::class.java) &&
    +                isEmbeddedMethod.isPublic &&
    +                isEmbeddedMethod.doesReturn(Boolean::class.java) &&
    +                getTaskBoundsMethod.isPublic &&
    +                getTaskBoundsMethod.doesReturn(Rect::class.java) &&
    +                getActivityStackBoundsMethod.isPublic &&
    +                getActivityStackBoundsMethod.doesReturn(Rect::class.java)
    +        }
    +
    +    private fun isClassActivityStackAttributesValid(): Boolean =
    +        validateReflection("Class ActivityStackAttributes is not valid") {
    +            val activityStackAttributesClass = ActivityStackAttributes::class.java
    +            val getRelativeBoundsMethod =
    +                activityStackAttributesClass.getMethod("getRelativeBounds")
    +            val getWindowAttributesMethod =
    +                activityStackAttributesClass.getMethod("getWindowAttributes")
    +            getRelativeBoundsMethod.isPublic &&
    +                getRelativeBoundsMethod.doesReturn(Rect::class.java) &&
    +                getWindowAttributesMethod.isPublic &&
    +                getWindowAttributesMethod.doesReturn(WindowAttributes::class.java)
    +        }
    +
    +    private fun isClassActivityStackAttributesBuilderValid(): Boolean =
    +        validateReflection("Class ActivityStackAttributes.Builder is not valid") {
    +            val activityStackAttributesBuilderClass = ActivityStackAttributes.Builder::class.java
    +            val activityStackAttributesBuilderConstructor =
    +                activityStackAttributesBuilderClass.getDeclaredConstructor()
    +            val setRelativeBoundsMethod = activityStackAttributesBuilderClass.getMethod(
    +                "setRelativeBounds",
    +                Rect::class.java
    +            )
    +            val setWindowAttributesMethod = activityStackAttributesBuilderClass.getMethod(
    +                "setWindowAttributes",
    +                WindowAttributes::class.java
    +            )
    +            activityStackAttributesBuilderConstructor.isPublic &&
    +                setRelativeBoundsMethod.isPublic &&
    +                setRelativeBoundsMethod.doesReturn(ActivityStackAttributes.Builder::class.java) &&
    +                setWindowAttributesMethod.isPublic &&
    +                setWindowAttributesMethod.doesReturn(ActivityStackAttributes.Builder::class.java)
    +        }
    +
    +    private fun isClassActivityStackAttributesCalculatorParamsValid(): Boolean =
    +        validateReflection("Class ActivityStackAttributesCalculatorParams is not valid") {
    +            val activityStackAttributesCalculatorParamsClass =
    +                ActivityStackAttributesCalculatorParams::class.java
    +            val getParentContainerInfoMethod =
    +                activityStackAttributesCalculatorParamsClass.getMethod("getParentContainerInfo")
    +            val getActivityStackTagMethod =
    +                activityStackAttributesCalculatorParamsClass.getMethod("getActivityStackTag")
    +            val getLaunchOptionsMethod =
    +                activityStackAttributesCalculatorParamsClass.getMethod("getLaunchOptions")
    +            getParentContainerInfoMethod.isPublic &&
    +                getParentContainerInfoMethod.doesReturn(ParentContainerInfo::class.java) &&
    +                getActivityStackTagMethod.isPublic &&
    +                getActivityStackTagMethod.doesReturn(String::class.java) &&
    +                getLaunchOptionsMethod.isPublic &&
    +                getLaunchOptionsMethod.doesReturn(Bundle::class.java)
             }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
    index ac627fc..d7e818b 100644
    --- a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
    
    @@ -19,8 +19,7 @@
     import android.annotation.SuppressLint
     import androidx.annotation.FloatRange
     import androidx.annotation.IntRange
    -import androidx.annotation.RestrictTo
    -import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
    +import androidx.window.RequiresWindowSdkExtension
     import androidx.window.WindowSdkExtensions
     import androidx.window.core.SpecificationComputer.Companion.startSpecification
     import androidx.window.core.VerificationMode
    @@ -40,6 +39,9 @@
      *   - Animation background color — The color of the background during
      *     animation of the split involving this `SplitAttributes` object if the
      *     animation requires a background
    + *   - Divider attributes — Specifies whether a divider is needed between
    + *     the split containers and the properties of the divider, including the
    + *     color, the width, whether the divider is draggable, etc.
      *
      * Attributes can be configured by:
      *   - Setting the default `SplitAttributes` using
    @@ -49,28 +51,33 @@
      *     `animationBackgroundColor` attributes in `` or
      *     `` tags in an XML configuration file. The
      *     attributes are parsed as [SplitType], [LayoutDirection], and
    - *     [BackgroundColor], respectively. Note that [SplitType.HingeSplitType]
    + *     [EmbeddingAnimationBackground], respectively. Note that [SplitType.HingeSplitType]
      *     is not supported XML format.
    - *   - Set `SplitAttributes` calculation function by
    - *     [SplitController.setSplitAttributesCalculator]
    - *     to customize the `SplitAttributes` for a given device and window state.
    + *   - Using
    + *     [SplitAttributesCalculator.computeSplitAttributesForParams] to customize
    + *     the `SplitAttributes` for a given device and window state.
    + *
    + * @property splitType The split type attribute. Defaults to an equal split of the parent window for
    + * the primary and secondary containers.
    + * @property layoutDirection The layout direction of the parent window split. The default is based
    + * on locale value.
    + * @property animationBackground The animation background to use during the animation of the split
    + * involving this `SplitAttributes` object if the animation requires a background. The default is to
    + * use the current theme window background color.
    + * @property dividerAttributes The [DividerAttributes] for this split. Defaults to
    + * [DividerAttributes.NO_DIVIDER], which means no divider is requested.
      *
      * @see SplitAttributes.SplitType
      * @see SplitAttributes.LayoutDirection
    + * @see EmbeddingAnimationBackground
    + * @see EmbeddingAnimationBackground.createColorBackground
    + * @see EmbeddingAnimationBackground.DEFAULT
      */
    -class SplitAttributes @RestrictTo(LIBRARY_GROUP) constructor(
    -
    -    /**
    -     * The split type attribute. Defaults to an equal split of the parent window
    -     * for the primary and secondary containers.
    -     */
    +class SplitAttributes @JvmOverloads constructor(
         val splitType: SplitType = SPLIT_TYPE_EQUAL,
    -
    -    /**
    -     * The layout direction attribute for the parent window split. The default
    -     * is based on locale.
    -     */
         val layoutDirection: LayoutDirection = LOCALE,
    +    val animationBackground: EmbeddingAnimationBackground = EmbeddingAnimationBackground.DEFAULT,
    +    val dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER,
     ) {
     
         /**
    @@ -365,6 +372,8 @@
         override fun hashCode(): Int {
             var result = splitType.hashCode()
             result = result * 31 + layoutDirection.hashCode()
    +        result = result * 31 + animationBackground.hashCode()
    +        result = result * 31 + dividerAttributes.hashCode()
             return result
         }
     
    @@ -380,7 +389,9 @@
             if (this === other) return true
             if (other !is SplitAttributes) return false
             return splitType == other.splitType &&
    -            layoutDirection == other.layoutDirection
    +            layoutDirection == other.layoutDirection &&
    +            animationBackground == other.animationBackground &&
    +            dividerAttributes == other.dividerAttributes
         }
     
         /**
    @@ -390,7 +401,9 @@
          */
         override fun toString(): String =
             "${SplitAttributes::class.java.simpleName}:" +
    -            "{splitType=$splitType, layoutDir=$layoutDirection }"
    +            "{splitType=$splitType, layoutDir=$layoutDirection, " +
    +            "animationBackground=$animationBackground, " +
    +            "dividerAttributes=$dividerAttributes }"
     
         /**
          * Builder for creating an instance of [SplitAttributes].
    @@ -400,10 +413,13 @@
          *  - The default layout direction is based on locale.
          *  - The default animation background color is to use the current theme
          *    window background color.
    +     *  - The default divider attributes is not to use divider.
          */
         class Builder {
             private var splitType = SPLIT_TYPE_EQUAL
             private var layoutDirection = LOCALE
    +        private var animationBackground = EmbeddingAnimationBackground.DEFAULT
    +        private var dividerAttributes: DividerAttributes = DividerAttributes.NO_DIVIDER
     
             /**
              * Sets the split type attribute.
    @@ -432,11 +448,39 @@
                 apply { this.layoutDirection = layoutDirection }
     
             /**
    +         * Sets the animation background to use during animation of the split involving this
    +         * `SplitAttributes` object if the animation requires a background.
    +         *
    +         * The default is [EmbeddingAnimationBackground.DEFAULT], which means to use the
    +         * current theme window background color.
    +         *
    +         * The [EmbeddingAnimationBackground] can be supported only if the vendor API level of
    +         * the target device is equals or higher than required API level. Otherwise, it would be
    +         * no-op when setting the [EmbeddingAnimationBackground] on a target device that has lower
    +         * API level.
    +         *
    +         * @param background The animation background.
    +         * @return This `Builder`.
    +         *
    +         * @see EmbeddingAnimationBackground.createColorBackground
    +         * @see EmbeddingAnimationBackground.DEFAULT
    +         */
    +        @RequiresWindowSdkExtension(5)
    +        fun setAnimationBackground(background: EmbeddingAnimationBackground): Builder =
    +            apply { animationBackground = background }
    +
    +        /** Sets the [DividerAttributes]. */
    +        @RequiresWindowSdkExtension(6)
    +        fun setDividerAttributes(dividerAttributes: DividerAttributes): Builder =
    +            apply { this.dividerAttributes = dividerAttributes }
    +
    +        /**
              * Builds a `SplitAttributes` instance with the attributes specified by
    -         * [setSplitType] and [setLayoutDirection].
    +         * [setSplitType], [setLayoutDirection], and [setAnimationBackground].
              *
              * @return The new `SplitAttributes` instance.
              */
    -        fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection)
    +        fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection,
    +            animationBackground, dividerAttributes)
         }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
    index 063bc0c..b6f4cb8 100644
    --- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
    
    @@ -22,7 +22,6 @@
     import androidx.window.RequiresWindowSdkExtension
     import androidx.window.WindowProperties
     import androidx.window.WindowSdkExtensions
    -import androidx.window.core.ExperimentalWindowApi
     import androidx.window.layout.WindowMetrics
     import kotlinx.coroutines.channels.awaitClose
     import kotlinx.coroutines.flow.Flow
    @@ -86,6 +85,55 @@
             get() = embeddingBackend.splitSupportStatus
     
         /**
    +     * Pins the top-most [ActivityStack] to keep the stack of the Activities to be always
    +     * positioned on top. The rest of the activities in the Task will be split with the pinned
    +     * [ActivityStack]. The pinned [ActivityStack] would also have isolated activity
    +     * navigation in which only the activities that are started from the pinned
    +     * [ActivityStack] can be added on top of the [ActivityStack].
    +     *
    +     * The pinned [ActivityStack] is unpinned whenever the pinned [ActivityStack] is expanded.
    +     * Use [SplitPinRule.Builder.setSticky] if the same [ActivityStack] should be pinned
    +     * again whenever the [ActivityStack] is on top and split with another [ActivityStack] again.
    +     *
    +     * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than
    +     * or equal to 5.
    +     *
    +     * @param taskId The id of the Task that top [ActivityStack] should be pinned.
    +     * @param splitPinRule The SplitRule that specifies how the top [ActivityStack] should
    +     *                     be split with others.
    +     * @return Returns `true` if the top [ActivityStack] is successfully pinned.
    +     *         Otherwise, `false`. Few examples are:
    +     *         1. There's no [ActivityStack].
    +     *         2. There is already an existing pinned [ActivityStack].
    +     *         3. There's no other [ActivityStack] to split with the top [ActivityStack].
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    +     *                                       is less than 5.
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    fun pinTopActivityStack(taskId: Int, splitPinRule: SplitPinRule): Boolean {
    +        return embeddingBackend.pinTopActivityStack(taskId, splitPinRule)
    +    }
    +
    +    /**
    +     * Unpins the pinned [ActivityStack]. The [ActivityStack] will still be the
    +     * top-most [ActivityStack] right after unpinned, and the [ActivityStack] could
    +     * be expanded or continue to be split with the next top [ActivityStack] if the current
    +     * state matches any of the existing [SplitPairRule]. It is a no-op call if the task
    +     * does not have a pinned [ActivityStack].
    +     *
    +     * The caller **must** make sure if [WindowSdkExtensions.extensionVersion] is greater than
    +     * or equal to 5.
    +     *
    +     * @param taskId The id of the Task that top [ActivityStack] should be unpinned.
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    +     *                                       is less than 5.
    +     */
    +    @RequiresWindowSdkExtension(5)
    +    fun unpinTopActivityStack(taskId: Int) {
    +        embeddingBackend.unpinTopActivityStack(taskId)
    +    }
    +
    +    /**
          * Sets or replaces the previously registered [SplitAttributes] calculator.
          *
          * **Note** that it's callers' responsibility to check if this API is supported by checking
    @@ -150,27 +198,6 @@
         }
     
         /**
    -     * Triggers a [SplitAttributes] update callback for the current topmost and visible split layout
    -     * if there is one. This method can be used when a change to the split presentation originates
    -     * from an application state change. Changes that are driven by parent window changes or new
    -     * activity starts invoke the callback provided in [setSplitAttributesCalculator] automatically
    -     * without the need to call this function.
    -     *
    -     * The top [SplitInfo] is usually the last element of [SplitInfo] list which was received from
    -     * the callback registered in [splitInfoList].
    -     *
    -     * The call will be ignored if there is no visible split.
    -     *
    -     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
    -     *                                       is less than 3.
    -     */
    -    @ExperimentalWindowApi
    -    @RequiresWindowSdkExtension(3)
    -    fun invalidateTopVisibleSplitAttributes() {
    -        embeddingBackend.invalidateTopVisibleSplitAttributes()
    -    }
    -
    -    /**
          * Updates the [SplitAttributes] of a split pair. This is an alternative to using
          * a split attributes calculator callback set in [setSplitAttributesCalculator], useful when
          * apps only need to update the splits in a few cases proactively but rely on the default split
    @@ -185,15 +212,15 @@
          * - A new Activity being launched.
          * - A window or device state updates (e,g. due to screen rotation or folding state update).
          *
    -     * In most cases it is suggested to use [invalidateTopVisibleSplitAttributes] if
    -     * [SplitAttributes] calculator callback is used.
    +     * In most cases it is suggested to use
    +     * [ActivityEmbeddingController.invalidateTopVisibleActivityStacks] if a calculator has been set
    +     * through [setSplitAttributesCalculator].
          *
          * @param splitInfo the split pair to update
          * @param splitAttributes the [SplitAttributes] to be applied
          * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion]
          *                                       is less than 3.
          */
    -    @ExperimentalWindowApi
         @RequiresWindowSdkExtension(3)
         fun updateSplitAttributes(splitInfo: SplitInfo, splitAttributes: SplitAttributes) {
             embeddingBackend.updateSplitAttributes(splitInfo, splitAttributes)
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
    index 362b032..56b6bb6d 100644
    --- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
    
    @@ -19,10 +19,13 @@
     import android.app.Activity
     import android.os.IBinder
     import androidx.annotation.RestrictTo
    -import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.WindowSdkExtensions
    +import androidx.window.extensions.embedding.SplitInfo.Token
     
     /** Describes a split pair of two containers with activities. */
    -class SplitInfo @RestrictTo(LIBRARY_GROUP) constructor(
    +@Suppress("Deprecation") // To compat with device with version 3 and 4.
    +class SplitInfo private constructor(
         /**
          * The [ActivityStack] representing the primary split container.
          */
    @@ -33,11 +36,79 @@
         val secondaryActivityStack: ActivityStack,
         /** The [SplitAttributes] of this split pair. */
         val splitAttributes: SplitAttributes,
    +
    +    @Deprecated(
    +        message = "Use [token] instead",
    +        replaceWith = ReplaceWith(
    +            expression = "SplitInfo.token",
    +            imports = arrayOf("androidx.window.embedding.SplitInfo"),
    +        )
    +    )
    +    private val binder: IBinder?,
         /**
          * A token uniquely identifying this `SplitInfo`.
          */
    -    internal val token: IBinder,
    +    private val token: Token?,
     ) {
    +    @RequiresWindowSdkExtension(5)
    +    internal constructor(
    +        primaryActivityStack: ActivityStack,
    +        secondaryActivityStack: ActivityStack,
    +        splitAttributes: SplitAttributes,
    +        token: Token,
    +    ) : this(primaryActivityStack, secondaryActivityStack, splitAttributes, binder = null, token)
    +
    +    /**
    +     * Creates SplitInfo for [WindowSdkExtensions.extensionVersion] 3 and 4.
    +     */
    +    @RequiresWindowSdkExtension(3)
    +    internal constructor(
    +        primaryActivityStack: ActivityStack,
    +        secondaryActivityStack: ActivityStack,
    +        splitAttributes: SplitAttributes,
    +        binder: IBinder,
    +    ) : this(
    +        primaryActivityStack,
    +        secondaryActivityStack,
    +        splitAttributes,
    +        binder,
    +        token = null,
    +    ) {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)
    +    }
    +
    +    /**
    +     * Creates SplitInfo ONLY for testing.
    +     *
    +     * @param primaryActivityStack the [ActivityStack] representing the primary split container.
    +     * @param secondaryActivityStack the [ActivityStack] representing the secondary split container.
    +     * @param splitAttributes the [SplitAttributes] of this split pair.
    +     */
    +    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    +    constructor(
    +        primaryActivityStack: ActivityStack,
    +        secondaryActivityStack: ActivityStack,
    +        splitAttributes: SplitAttributes,
    +    ) : this(
    +        primaryActivityStack,
    +        secondaryActivityStack,
    +        splitAttributes,
    +        binder = null,
    +        token = null,
    +    )
    +
    +    @RequiresWindowSdkExtension(3)
    +    internal fun getBinder(): IBinder = let {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(3..4)
    +        requireNotNull(binder)
    +    }
    +
    +    @RequiresWindowSdkExtension(5)
    +    internal fun getToken(): Token = let {
    +        WindowSdkExtensions.getInstance().requireExtensionVersion(5)
    +        requireNotNull(token)
    +    }
    +
         /**
          * Whether the [primaryActivityStack] or the [secondaryActivityStack] in this [SplitInfo]
          * contains the [activity].
    @@ -55,6 +126,7 @@
             if (secondaryActivityStack != other.secondaryActivityStack) return false
             if (splitAttributes != other.splitAttributes) return false
             if (token != other.token) return false
    +        if (binder != other.binder) return false
     
             return true
         }
    @@ -64,6 +136,7 @@
             result = 31 * result + secondaryActivityStack.hashCode()
             result = 31 * result + splitAttributes.hashCode()
             result = 31 * result + token.hashCode()
    +        result = 31 * result + binder.hashCode()
             return result
         }
     
    @@ -73,7 +146,12 @@
                 append("primaryActivityStack=$primaryActivityStack, ")
                 append("secondaryActivityStack=$secondaryActivityStack, ")
                 append("splitAttributes=$splitAttributes, ")
    -            append("token=$token")
    +            if (token != null) {
    +                append("token=$token")
    +            }
    +            if (binder != null) {
    +                append("binder=$binder")
    +            }
                 append("}")
             }
         }
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
    new file mode 100644
    index 0000000..d4dc97f
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/embedding/SplitPinRule.kt
    
    @@ -0,0 +1,234 @@
    +/*
    + * 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 androidx.window.embedding
    +
    +import androidx.annotation.IntRange
    +import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
    +import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
    +import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW
    +import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_DP_DEFAULT
    +
    +/**
    + * Split configuration rules for pinning an [ActivityStack]. Define how the pinned [ActivityStack]
    + * should be displayed side-by-side with the other [ActivityStack].
    + */
    +class SplitPinRule internal constructor(
    +    /**
    +     * A unique string to identify this [SplitPinRule].
    +     */
    +    tag: String? = null,
    +    /**
    +     * The default [SplitAttributes] to apply on the pin container and the paired container.
    +     */
    +    defaultSplitAttributes: SplitAttributes,
    +    /**
    +     * Whether this rule should be sticky. If the value is `false`, this rule be removed whenever
    +     * the pinned [ActivityStack] is unpinned. Set to `true` if the rule should be applied
    +     * whenever once again possible (e.g. the host Task bounds satisfies the size and aspect ratio
    +     * requirements).
    +     */
    +    val isSticky: Boolean,
    +
    +    @IntRange(from = 0) minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
    +    @IntRange(from = 0) minHeightDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
    +    @IntRange(from = 0) minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
    +    maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,
    +    maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
    +) : SplitRule(tag, minWidthDp, minHeightDp, minSmallestWidthDp, maxAspectRatioInPortrait,
    +    maxAspectRatioInLandscape, defaultSplitAttributes) {
    +
    +    /**
    +     * Builder for [SplitPinRule].
    +     *
    +     */
    +    class Builder {
    +        private var tag: String? = null
    +        @IntRange(from = 0)
    +        private var minWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
    +        @IntRange(from = 0)
    +        private var minHeightDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
    +        @IntRange(from = 0)
    +        private var minSmallestWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
    +        private var maxAspectRatioInPortrait = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
    +        private var maxAspectRatioInLandscape = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
    +        private var defaultSplitAttributes = SplitAttributes.Builder().build()
    +        private var isSticky: Boolean = false
    +
    +        /**
    +         * Sets the smallest value of width of the parent window when the split should be used, in
    +         * DP.
    +         * When the window size is smaller than requested here, activities in the secondary
    +         * container will be stacked on top of the activities in the primary one, completely
    +         * overlapping them.
    +         *
    +         * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
    +         * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
    +         *
    +         * @param minWidthDp the smallest value of width of the parent window when the split should
    +         * be used, in DP.
    +         */
    +        fun setMinWidthDp(@IntRange(from = 0) minWidthDp: Int): Builder =
    +            apply { this.minWidthDp = minWidthDp }
    +
    +        /**
    +         * Sets the smallest value of height of the parent task window when the split should be
    +         * used, in DP. When the window size is smaller than requested here, activities in the
    +         * secondary container will be stacked on top of the activities in the primary one,
    +         * completely overlapping them.
    +         *
    +         * It is useful if it's necessary to split the parent window horizontally for this
    +         * [SplitPinRule].
    +         *
    +         * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
    +         * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
    +         *
    +         * @param minHeightDp the smallest value of height of the parent task window when the split
    +         * should be used, in DP.
    +         *
    +         * @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
    +         * @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
    +         */
    +        fun setMinHeightDp(@IntRange(from = 0) minHeightDp: Int): Builder =
    +            apply { this.minHeightDp = minHeightDp }
    +
    +        /**
    +         * Sets the smallest value of the smallest possible width of the parent window in any
    +         * rotation when the split should be used, in DP. When the window size is smaller than
    +         * requested here, activities in the secondary container will be stacked on top of the
    +         * activities in the primary one, completely overlapping them.
    +         *
    +         * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
    +         * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
    +         *
    +         * @param minSmallestWidthDp the smallest value of the smallest possible width of the parent
    +         * window in any rotation when the split should be used, in DP.
    +         */
    +        fun setMinSmallestWidthDp(@IntRange(from = 0) minSmallestWidthDp: Int): Builder =
    +            apply { this.minSmallestWidthDp = minSmallestWidthDp }
    +
    +        /**
    +         * Sets the largest value of the aspect ratio, expressed as `height / width` in decimal
    +         * form, of the parent window bounds in portrait when the split should be used. When the
    +         * window aspect ratio is greater than requested here, activities in the secondary container
    +         * will be stacked on top of the activities in the primary one, completely overlapping them.
    +         *
    +         * This value is only used when the parent window is in portrait (height >= width).
    +         *
    +         * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is
    +         * the recommend value to only allow split when the parent window is not too stretched in
    +         * portrait.
    +         *
    +         * @param aspectRatio the largest value of the aspect ratio, expressed as `height / width`
    +         * in decimal form, of the parent window bounds in portrait when the split should be used.
    +         *
    +         * @see EmbeddingAspectRatio.ratio
    +         * @see EmbeddingAspectRatio.ALWAYS_ALLOW
    +         * @see EmbeddingAspectRatio.ALWAYS_DISALLOW
    +         */
    +        fun setMaxAspectRatioInPortrait(aspectRatio: EmbeddingAspectRatio): Builder =
    +            apply { this.maxAspectRatioInPortrait = aspectRatio }
    +
    +        /**
    +         * Sets the largest value of the aspect ratio, expressed as `width / height` in decimal
    +         * form, of the parent window bounds in landscape when the split should be used. When the
    +         * window aspect ratio is greater than requested here, activities in the secondary container
    +         * will be stacked on top of the activities in the primary one, completely overlapping them.
    +         *
    +         * This value is only used when the parent window is in landscape (width > height).
    +         *
    +         * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which
    +         * is the recommend value to always allow split when the parent window is in landscape.
    +         *
    +         * @param aspectRatio the largest value of the aspect ratio, expressed as `width / height`
    +         * in decimal form, of the parent window bounds in landscape when the split should be used.
    +         *
    +         * @see EmbeddingAspectRatio.ratio
    +         * @see EmbeddingAspectRatio.ALWAYS_ALLOW
    +         * @see EmbeddingAspectRatio.ALWAYS_DISALLOW
    +         */
    +        fun setMaxAspectRatioInLandscape(aspectRatio: EmbeddingAspectRatio): Builder =
    +            apply { this.maxAspectRatioInLandscape = aspectRatio }
    +
    +        /**
    +         * Sets the default [SplitAttributes] to apply on the activity containers pair when the host
    +         * task bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp],
    +         * [maxAspectRatioInPortrait] and [maxAspectRatioInLandscape] requirements.
    +         *
    +         * @param defaultSplitAttributes the default [SplitAttributes] to apply on the activity
    +         * containers pair when the host task bounds satisfy all the rule requirements.
    +         */
    +        fun setDefaultSplitAttributes(defaultSplitAttributes: SplitAttributes): Builder =
    +            apply { this.defaultSplitAttributes = defaultSplitAttributes }
    +
    +        /**
    +         * Sets a unique string to identify this [SplitPinRule], which defaults to `null`.
    +         * The suggested usage is to set the tag to be able to differentiate between different rules
    +         * in the [SplitAttributesCalculatorParams.splitRuleTag].
    +         *
    +         * @param tag unique string to identify this [SplitPinRule].
    +         */
    +        fun setTag(tag: String?): Builder =
    +            apply { this.tag = tag }
    +
    +        /**
    +         * Sets this rule to be sticky.
    +         * @see isSticky
    +         *
    +         * @param isSticky whether to be a sticky rule.
    +         */
    +        fun setSticky(isSticky: Boolean): Builder =
    +            apply { this.isSticky = isSticky }
    +
    +        /**
    +         * Builds a [SplitPinRule] instance.
    +         *
    +         * @return The new [SplitPinRule] instance.
    +         */
    +        fun build() = SplitPinRule(
    +            tag,
    +            defaultSplitAttributes,
    +            isSticky,
    +            minWidthDp,
    +            minHeightDp,
    +            minSmallestWidthDp,
    +            maxAspectRatioInPortrait,
    +            maxAspectRatioInLandscape
    +        )
    +    }
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) return true
    +        if (other !is SplitPinRule) return false
    +
    +        if (!super.equals(other)) return false
    +        if (isSticky != other.isSticky) return false
    +        return true
    +    }
    +
    +    override fun hashCode(): Int {
    +        var result = super.hashCode()
    +        result = 31 * result + isSticky.hashCode()
    +        return result
    +    }
    +
    +    override fun toString(): String =
    +        "${SplitPinRule::class.java.simpleName}{" +
    +            "tag=$tag" +
    +            ", defaultSplitAttributes=$defaultSplitAttributes" +
    +            ", isSticky=$isSticky" +
    +            "}"
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
    index 75d2af5..2b11df7 100644
    --- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
    +++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
    
    @@ -133,6 +133,8 @@
          * The default [SplitAttributes] to apply on the activity containers pair when the host task
          * bounds satisfy [minWidthDp], [minHeightDp], [minSmallestWidthDp],
          * [maxAspectRatioInPortrait] and [maxAspectRatioInLandscape] requirements.
    +     *
    +     * It is set to split the host parent task vertically and equally by default.
          */
         val defaultSplitAttributes: SplitAttributes,
     ) : EmbeddingRule(tag) {
    
    diff --git a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
    index 047a14c..dcaf087 100644
    --- a/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
    +++ b/window/window/src/main/java/androidx/window/layout/SafeWindowLayoutComponentProvider.kt
    
    @@ -30,10 +30,13 @@
     import androidx.window.reflection.ReflectionUtils.doesReturn
     import androidx.window.reflection.ReflectionUtils.isPublic
     import androidx.window.reflection.ReflectionUtils.validateReflection
    +import androidx.window.reflection.WindowExtensionsConstants.DISPLAY_FOLD_FEATURE_CLASS
     import androidx.window.reflection.WindowExtensionsConstants.FOLDING_FEATURE_CLASS
     import androidx.window.reflection.WindowExtensionsConstants.JAVA_CONSUMER
    +import androidx.window.reflection.WindowExtensionsConstants.SUPPORTED_WINDOW_FEATURES_CLASS
     import androidx.window.reflection.WindowExtensionsConstants.WINDOW_CONSUMER
     import androidx.window.reflection.WindowExtensionsConstants.WINDOW_LAYOUT_COMPONENT_CLASS
    +import java.lang.reflect.ParameterizedType
     
     /**
      * Reflection Guard for [WindowLayoutComponent].
    @@ -63,13 +66,12 @@
             if (!isWindowLayoutComponentAccessible()) {
                 return false
             }
    -        // TODO(b/267831038): can fallback to VendorApiLevel1 when level2 is not match
    -        //  but level 1 is matched
    -        return when (ExtensionsUtil.safeVendorApiLevel) {
    -            1 -> hasValidVendorApiLevel1()
    -            in 2..Int.MAX_VALUE -> hasValidVendorApiLevel2()
    -            // TODO(b/267956499): add hasValidVendorApiLevel3
    -            else -> false
    +        val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
    +        return when {
    +            vendorApiLevel < 1 -> false
    +            vendorApiLevel == 1 -> hasValidVendorApiLevel1()
    +            vendorApiLevel < 5 -> hasValidVendorApiLevel2()
    +            else -> hasValidVendorApiLevel6()
             }
         }
     
    @@ -100,8 +102,17 @@
             return hasValidVendorApiLevel1() && isMethodWindowLayoutInfoListenerWindowConsumerValid()
         }
     
    +    @VisibleForTesting
    +    internal fun hasValidVendorApiLevel6(): Boolean {
    +        return hasValidVendorApiLevel2() &&
    +            isDisplayFoldFeatureValid() &&
    +            isSupportedWindowFeaturesValid() &&
    +            isGetSupportedWindowFeaturesValid()
    +    }
    +
         private fun isWindowLayoutProviderValid(): Boolean {
    -        return validateReflection("WindowExtensions#getWindowLayoutComponent is not valid") {
    +        return validateReflection(
    +            "WindowExtensions#getWindowLayoutComponent is not valid") {
                 val extensionsClass = safeWindowExtensionsProvider.windowExtensionsClass
                 val getWindowLayoutComponentMethod =
                     extensionsClass.getMethod("getWindowLayoutComponent")
    @@ -164,6 +175,63 @@
             }
         }
     
    +    private fun isDisplayFoldFeatureValid(): Boolean {
    +        return validateReflection("DisplayFoldFeature is not valid") {
    +            val displayFoldFeatureClass = displayFoldFeatureClass
    +
    +            val getTypeMethod = displayFoldFeatureClass.getMethod("getType")
    +            val hasPropertyMethod = displayFoldFeatureClass
    +                .getMethod("hasProperty", Int::class.java)
    +            val hasPropertiesMethod = displayFoldFeatureClass
    +                .getMethod("hasProperties", IntArray::class.java)
    +
    +            getTypeMethod.isPublic &&
    +                getTypeMethod.doesReturn(Int::class.java) &&
    +                hasPropertyMethod.isPublic &&
    +                hasPropertyMethod.doesReturn(Boolean::class.java) &&
    +                hasPropertiesMethod.isPublic &&
    +                hasPropertiesMethod.doesReturn(Boolean::class.java)
    +        }
    +    }
    +
    +    private fun isSupportedWindowFeaturesValid(): Boolean {
    +        return validateReflection("SupportedWindowFeatures is not valid") {
    +            val supportedWindowFeaturesClass = supportedWindowFeaturesClass
    +
    +            val getDisplayFoldFeaturesMethod = supportedWindowFeaturesClass
    +                .getMethod("getDisplayFoldFeatures")
    +            val returnTypeGeneric =
    +                (getDisplayFoldFeaturesMethod.genericReturnType as ParameterizedType)
    +                .actualTypeArguments[0] as Class<*>
    +
    +            getDisplayFoldFeaturesMethod.isPublic &&
    +                getDisplayFoldFeaturesMethod.doesReturn(List::class.java) &&
    +                returnTypeGeneric == displayFoldFeatureClass
    +        }
    +    }
    +
    +    private fun isGetSupportedWindowFeaturesValid(): Boolean {
    +        return validateReflection(
    +            "WindowLayoutComponent#getSupportedWindowFeatures is not valid") {
    +            val windowLayoutComponent = windowLayoutComponentClass
    +            val getSupportedWindowFeaturesMethod = windowLayoutComponent
    +                .getMethod("getSupportedWindowFeatures")
    +
    +            getSupportedWindowFeaturesMethod.isPublic &&
    +                getSupportedWindowFeaturesMethod.doesReturn(supportedWindowFeaturesClass)
    +        }
    +    }
    +
    +    private val displayFoldFeatureClass: Class<*>
    +        get() {
    +            return loader.loadClass(DISPLAY_FOLD_FEATURE_CLASS)
    +        }
    +
    +    private val supportedWindowFeaturesClass: Class<*>
    +        get() {
    +            return loader.loadClass(SUPPORTED_WINDOW_FEATURES_CLASS)
    +        }
    +
         private val foldingFeatureClass: Class<*>
             get() {
                 return loader.loadClass(FOLDING_FEATURE_CLASS)
    
    diff --git a/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
    new file mode 100644
    index 0000000..167566a
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/layout/SupportedPosture.kt
    
    @@ -0,0 +1,52 @@
    +/*
    + * 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 androidx.window.layout
    +
    +/**
    + * A class to represent a posture that the device supports.
    + */
    +class SupportedPosture internal constructor(private val rawValue: Int) {
    +
    +    override fun toString(): String {
    +        return when (this) {
    +            TABLETOP -> "TABLETOP"
    +            else -> "UNKNOWN"
    +        }
    +    }
    +
    +    override fun equals(other: Any?): Boolean {
    +        if (this === other) return true
    +        if (other == null) return false
    +        if (other::class != SupportedPosture::class) return false
    +
    +        other as SupportedPosture
    +
    +        return rawValue == other.rawValue
    +    }
    +
    +    override fun hashCode(): Int {
    +        return rawValue
    +    }
    +
    +    companion object {
    +        /**
    +         * The posture where there is a single fold in the half-opened state.
    +         */
    +        @JvmField
    +        val TABLETOP: SupportedPosture = SupportedPosture(0)
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
    index 06ca64f..8ce03ec 100644
    --- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
    +++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
    
    @@ -23,6 +23,7 @@
     import androidx.annotation.RestrictTo
     import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
     import androidx.annotation.UiContext
    +import androidx.window.RequiresWindowSdkExtension
     import androidx.window.WindowSdkExtensions
     import androidx.window.core.ConsumerAdapter
     import androidx.window.layout.adapter.WindowBackend
    @@ -97,6 +98,20 @@
          */
         fun windowLayoutInfo(activity: Activity): Flow
     
    +    /**
    +     * Returns the [List] of [SupportedPosture] values. This value will not change during runtime.
    +     * These values are for determining if the device supports the given [SupportedPosture] but does
    +     * not mean the device is in the given [SupportedPosture]. Use [windowLayoutInfo] to determine
    +     * the current state of the [DisplayFeature]'s on the device.
    +     * @see windowLayoutInfo
    +     * @throws UnsupportedOperationException if [WindowSdkExtensions.extensionVersion] is less than
    +     * 6.
    +     * @throws NotImplementedError if a derived test class does not override this method.
    +     */
    +    @RequiresWindowSdkExtension(version = 6)
    +    val supportedPostures: List
    +        get() { throw NotImplementedError("Method was not implemented.") }
    +
         companion object {
     
             private val DEBUG = false
    @@ -133,7 +148,11 @@
             @JvmStatic
             fun getOrCreate(context: Context): WindowInfoTracker {
                 val backend = extensionBackend ?: SidecarWindowBackend.getInstance(context)
    -            val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, backend)
    +            val repo = WindowInfoTrackerImpl(
    +                WindowMetricsCalculatorCompat(),
    +                backend,
    +                WindowSdkExtensions.getInstance()
    +            )
                 return decorator.decorate(repo)
             }
     
    
    diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
    index 9424cae..fbdf834 100644
    --- a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
    +++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
    
    @@ -20,6 +20,7 @@
     import android.content.Context
     import androidx.annotation.UiContext
     import androidx.core.util.Consumer
    +import androidx.window.WindowSdkExtensions
     import androidx.window.layout.adapter.WindowBackend
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.channels.awaitClose
    @@ -36,7 +37,8 @@
      */
     internal class WindowInfoTrackerImpl(
         private val windowMetricsCalculator: WindowMetricsCalculator,
    -    private val windowBackend: WindowBackend
    +    private val windowBackend: WindowBackend,
    +    private val windowSdkExtensions: WindowSdkExtensions
     ) : WindowInfoTracker {
     
         /**
    @@ -65,4 +67,10 @@
                 }
             }.flowOn(Dispatchers.Main)
         }
    +
    +    override val supportedPostures: List
    +        get() {
    +            windowSdkExtensions.requireExtensionVersion(6)
    +            return windowBackend.supportedPostures
    +        }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
    index 1de2e40..84db3bb 100644
    --- a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
    +++ b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
    
    @@ -17,6 +17,7 @@
     
     import android.graphics.Rect
     import android.os.Build.VERSION_CODES
    +import android.util.DisplayMetrics
     import androidx.annotation.RequiresApi
     import androidx.annotation.RestrictTo
     import androidx.core.view.WindowInsetsCompat
    @@ -34,7 +35,14 @@
      */
     class WindowMetrics internal constructor(
         private val _bounds: Bounds,
    -    private val _windowInsetsCompat: WindowInsetsCompat
    +    private val _windowInsetsCompat: WindowInsetsCompat,
    +
    +    /**
    +     * Returns the logical density of the display this window is in.
    +     *
    +     * @see [DisplayMetrics.density]
    +     */
    +    val density: Float
     ) {
     
         /**
    @@ -43,8 +51,9 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         constructor(
             bounds: Rect,
    -        insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build()
    -        ) : this(Bounds(bounds), insets)
    +        insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build(),
    +        density: Float
    +    ) : this(Bounds(bounds), insets, density)
         /**
          * Returns a new [Rect] describing the bounds of the area the window occupies.
          *
    @@ -71,6 +80,7 @@
     
             if (_bounds != other._bounds) return false
             if (_windowInsetsCompat != other._windowInsetsCompat) return false
    +        if (density != other.density) return false
     
             return true
         }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
    index 3a10007..61546e8 100644
    --- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
    +++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
    
    @@ -28,6 +28,7 @@
     import androidx.annotation.UiContext
     import androidx.core.view.WindowInsetsCompat
     import androidx.window.core.Bounds
    +import androidx.window.layout.util.WindowMetricsCompatHelper
     
     /**
      * An interface to calculate the [WindowMetrics] for an [Activity] or a [UiContext].
    @@ -126,10 +127,11 @@
     
             private var decorator: (WindowMetricsCalculator) -> WindowMetricsCalculator =
                 { it }
    +        private val windowMetricsCalculatorCompat = WindowMetricsCalculatorCompat()
     
             @JvmStatic
             fun getOrCreate(): WindowMetricsCalculator {
    -            return decorator(WindowMetricsCalculatorCompat)
    +            return decorator(windowMetricsCalculatorCompat)
             }
     
             @JvmStatic
    @@ -148,18 +150,20 @@
              * Converts [Android API WindowMetrics][AndroidWindowMetrics] to
              * [Jetpack version WindowMetrics][WindowMetrics]
              */
    -        @Suppress("ClassVerificationFailure")
             @RequiresApi(Build.VERSION_CODES.R)
    -        internal fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics): WindowMetrics =
    -            WindowMetrics(
    -                windowMetrics.bounds,
    -                WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets)
    -            )
    +        internal fun translateWindowMetrics(
    +            windowMetrics: AndroidWindowMetrics,
    +            density: Float
    +        ): WindowMetrics {
    +            return WindowMetricsCompatHelper.getInstance()
    +                .translateWindowMetrics(windowMetrics, density)
    +        }
     
             internal fun fromDisplayMetrics(displayMetrics: DisplayMetrics): WindowMetrics {
                 return WindowMetrics(
                         Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels),
    -                    WindowInsetsCompat.Builder().build()
    +                    WindowInsetsCompat.Builder().build(),
    +                    displayMetrics.density
                     )
             }
         }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
    index c367096..f4c1a24 100644
    --- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
    +++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
    
    @@ -15,42 +15,20 @@
      */
     package androidx.window.layout
     
    -import android.annotation.SuppressLint
     import android.app.Activity
     import android.content.Context
    -import android.content.res.Configuration
    -import android.graphics.Point
    -import android.graphics.Rect
     import android.inputmethodservice.InputMethodService
    -import android.os.Build
    -import android.os.Build.VERSION_CODES
    -import android.util.Log
    -import android.view.Display
    -import android.view.DisplayCutout
    -import android.view.WindowManager
    -import androidx.annotation.RequiresApi
     import androidx.annotation.UiContext
    -import androidx.annotation.VisibleForTesting
     import androidx.core.view.WindowInsetsCompat
    -import androidx.window.core.Bounds
    -import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode
    -import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext
    -import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowBounds
    -import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets
    -import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowMetrics
    -import androidx.window.layout.util.ContextCompatHelperApi30.maximumWindowBounds
    -import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
    -import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
    -import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight
    -import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop
    -import java.lang.reflect.InvocationTargetException
    +import androidx.window.layout.util.DensityCompatHelper
    +import androidx.window.layout.util.WindowMetricsCompatHelper
     
     /**
      * Helper class used to compute window metrics across Android versions.
      */
    -internal object WindowMetricsCalculatorCompat : WindowMetricsCalculator {
    -
    -    private val TAG: String = WindowMetricsCalculatorCompat::class.java.simpleName
    +internal class WindowMetricsCalculatorCompat(
    +    private val densityCompatHelper: DensityCompatHelper = DensityCompatHelper.getInstance()
    +) : WindowMetricsCalculator {
     
         /**
          * Computes the current [WindowMetrics] for a given [Context]. The context can be either
    @@ -59,33 +37,8 @@
          * @see WindowMetricsCalculator.computeCurrentWindowMetrics
          */
         override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
    -        // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable
    -        if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
    -            return currentWindowMetrics(context)
    -        } else {
    -            when (val unwrappedContext = unwrapUiContext(context)) {
    -                is Activity -> {
    -                    return computeCurrentWindowMetrics(unwrappedContext)
    -                }
    -                is InputMethodService -> {
    -                    val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    -
    -                    // On older SDK levels, the app and IME could show up on different displays.
    -                    // However, there isn't a way for us to figure this out from the application
    -                    // layer. But, this should be good enough for now given the small likelihood of
    -                    // IMEs showing up on non-primary displays on these SDK levels.
    -                    @Suppress("DEPRECATION")
    -                    val displaySize = getRealSizeForDisplay(wm.defaultDisplay)
    -
    -                    // IME occupies the whole display bounds.
    -                    val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)
    -                    return WindowMetrics(imeBounds)
    -                }
    -                else -> {
    -                    throw IllegalArgumentException("$context is not a UiContext")
    -                }
    -            }
    -        }
    +        return WindowMetricsCompatHelper.getInstance()
    +            .currentWindowMetrics(context, densityCompatHelper)
         }
     
         /**
    @@ -93,24 +46,8 @@
          * @see WindowMetricsCalculator.computeCurrentWindowMetrics
          */
         override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
    -        val bounds = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
    -            currentWindowBounds(activity)
    -        } else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
    -            computeWindowBoundsQ(activity)
    -        } else if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
    -            computeWindowBoundsP(activity)
    -        } else if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
    -            computeWindowBoundsN(activity)
    -        } else {
    -            computeWindowBoundsIceCreamSandwich(activity)
    -        }
    -        // TODO (b/233899790): compute insets for other platform versions below R
    -        val windowInsetsCompat = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
    -            computeWindowInsetsCompat(activity)
    -        } else {
    -            WindowInsetsCompat.Builder().build()
    -        }
    -        return WindowMetrics(Bounds(bounds), windowInsetsCompat)
    +        return WindowMetricsCompatHelper.getInstance()
    +            .currentWindowMetrics(activity, densityCompatHelper)
         }
     
         /**
    @@ -118,7 +55,8 @@
          * @see WindowMetricsCalculator.computeMaximumWindowMetrics
          */
         override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
    -        return computeMaximumWindowMetrics(activity as Context)
    +        return WindowMetricsCompatHelper.getInstance()
    +            .maximumWindowMetrics(activity, densityCompatHelper)
         }
     
         /**
    @@ -126,305 +64,8 @@
          * @See WindowMetricsCalculator.computeMaximumWindowMetrics
          */
         override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
    -        // TODO(b/259148796): Make WindowMetricsCalculatorCompat more testable
    -        val bounds = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
    -            maximumWindowBounds(context)
    -        } else {
    -            val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    -            // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    -            // compatibility with older versions, as we can't reliably get the display associated
    -            // with a Context through public APIs either.
    -            @Suppress("DEPRECATION")
    -            val display = wm.defaultDisplay
    -            val displaySize = getRealSizeForDisplay(display)
    -            Rect(0, 0, displaySize.x, displaySize.y)
    -        }
    -        // TODO (b/233899790): compute insets for other platform versions below R
    -        val windowInsetsCompat = if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
    -            computeWindowInsetsCompat(context)
    -        } else {
    -            WindowInsetsCompat.Builder().build()
    -        }
    -        return WindowMetrics(Bounds(bounds), windowInsetsCompat)
    -    }
    -
    -    /** Computes the window bounds for [Build.VERSION_CODES.Q].  */
    -    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
    -    @RequiresApi(VERSION_CODES.Q)
    -    internal fun computeWindowBoundsQ(activity: Activity): Rect {
    -        var bounds: Rect
    -        val config = activity.resources.configuration
    -        try {
    -            val windowConfigField =
    -                Configuration::class.java.getDeclaredField("windowConfiguration")
    -            windowConfigField.isAccessible = true
    -            val windowConfig = windowConfigField[config]
    -            val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")
    -            bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)
    -        } catch (e: NoSuchFieldException) {
    -            Log.w(TAG, e)
    -            // If reflection fails for some reason default to the P implementation which still
    -            // has the ability to account for display cutouts.
    -            bounds = computeWindowBoundsP(activity)
    -        } catch (e: NoSuchMethodException) {
    -            Log.w(TAG, e)
    -            bounds = computeWindowBoundsP(activity)
    -        } catch (e: IllegalAccessException) {
    -            Log.w(TAG, e)
    -            bounds = computeWindowBoundsP(activity)
    -        } catch (e: InvocationTargetException) {
    -            Log.w(TAG, e)
    -            bounds = computeWindowBoundsP(activity)
    -        }
    -        return bounds
    -    }
    -
    -    /**
    -     * Computes the window bounds for [Build.VERSION_CODES.P].
    -     *
    -     *
    -     * NOTE: This method may result in incorrect values if the [android.content.res.Resources]
    -     * value stored at 'navigation_bar_height' does not match the true navigation bar inset on
    -     * the window.
    -     *
    -     */
    -    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
    -    @RequiresApi(VERSION_CODES.P)
    -    internal fun computeWindowBoundsP(activity: Activity): Rect {
    -        val bounds = Rect()
    -        val config = activity.resources.configuration
    -        try {
    -            val windowConfigField =
    -                Configuration::class.java.getDeclaredField("windowConfiguration")
    -            windowConfigField.isAccessible = true
    -            val windowConfig = windowConfigField[config]
    -
    -            // In multi-window mode we'll use the WindowConfiguration#mBounds property which
    -            // should match the window size. Otherwise we'll use the mAppBounds property and
    -            // will adjust it below.
    -            if (isInMultiWindowMode(activity)) {
    -                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")
    -                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
    -            } else {
    -                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")
    -                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
    -            }
    -        } catch (e: NoSuchFieldException) {
    -            Log.w(TAG, e)
    -            getRectSizeFromDisplay(activity, bounds)
    -        } catch (e: NoSuchMethodException) {
    -            Log.w(TAG, e)
    -            getRectSizeFromDisplay(activity, bounds)
    -        } catch (e: IllegalAccessException) {
    -            Log.w(TAG, e)
    -            getRectSizeFromDisplay(activity, bounds)
    -        } catch (e: InvocationTargetException) {
    -            Log.w(TAG, e)
    -            getRectSizeFromDisplay(activity, bounds)
    -        }
    -        val platformWindowManager = activity.windowManager
    -
    -        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    -        // compatibility with older versions
    -        @Suppress("DEPRECATION")
    -        val currentDisplay = platformWindowManager.defaultDisplay
    -        val realDisplaySize = Point()
    -        @Suppress("DEPRECATION")
    -        currentDisplay.getRealSize(realDisplaySize)
    -
    -        if (!isInMultiWindowMode(activity)) {
    -            // The activity is not in multi-window mode. Check if the addition of the
    -            // navigation bar size to mAppBounds results in the real display size and if so
    -            // assume the nav bar height should be added to the result.
    -            val navigationBarHeight = getNavigationBarHeight(activity)
    -            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
    -                bounds.bottom += navigationBarHeight
    -            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
    -                bounds.right += navigationBarHeight
    -            } else if (bounds.left == navigationBarHeight) {
    -                bounds.left = 0
    -            }
    -        }
    -        if ((bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&
    -            !isInMultiWindowMode(activity)
    -        ) {
    -            // If the corrected bounds are not the same as the display size and the activity is
    -            // not in multi-window mode it is possible there are unreported cutouts inset-ing
    -            // the window depending on the layoutInCutoutMode. Check for them here by getting
    -            // the cutout from the display itself.
    -            val displayCutout = getCutoutForDisplay(currentDisplay)
    -            if (displayCutout != null) {
    -                if (bounds.left == safeInsetLeft(displayCutout)) {
    -                    bounds.left = 0
    -                }
    -                if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {
    -                    bounds.right += safeInsetRight(displayCutout)
    -                }
    -                if (bounds.top == safeInsetTop(displayCutout)) {
    -                    bounds.top = 0
    -                }
    -                if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {
    -                    bounds.bottom += safeInsetBottom(displayCutout)
    -                }
    -            }
    -        }
    -        return bounds
    -    }
    -
    -    private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {
    -        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    -        // compatibility with older versions
    -        @Suppress("DEPRECATION")
    -        val defaultDisplay = activity.windowManager.defaultDisplay
    -        // [Display#getRectSize] is deprecated but we have this for
    -        // compatibility with older versions
    -        @Suppress("DEPRECATION")
    -        defaultDisplay.getRectSize(bounds)
    -    }
    -
    -    /**
    -     * Computes the window bounds for platforms between [Build.VERSION_CODES.N]
    -     * and [Build.VERSION_CODES.O_MR1], inclusive.
    -     *
    -     *
    -     * NOTE: This method may result in incorrect values under the following conditions:
    -     *
    -     *  * If the activity is in multi-window mode the origin of the returned bounds will
    -     * always be anchored at (0, 0).
    -     *  * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does
    -     *  not match the true navigation bar size the returned bounds will not take into account
    -     *  the navigation
    -     * bar.
    -     *
    -     */
    -    @RequiresApi(VERSION_CODES.N)
    -    internal fun computeWindowBoundsN(activity: Activity): Rect {
    -        val bounds = Rect()
    -        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    -        // compatibility with older versions
    -        @Suppress("DEPRECATION")
    -        val defaultDisplay = activity.windowManager.defaultDisplay
    -        // [Display#getRectSize] is deprecated but we have this for
    -        // compatibility with older versions
    -        @Suppress("DEPRECATION")
    -        defaultDisplay.getRectSize(bounds)
    -        if (!isInMultiWindowMode(activity)) {
    -            // The activity is not in multi-window mode. Check if the addition of the
    -            // navigation bar size to Display#getSize() results in the real display size and
    -            // if so return this value. If not, return the result of Display#getSize().
    -            val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
    -            val navigationBarHeight = getNavigationBarHeight(activity)
    -            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
    -                bounds.bottom += navigationBarHeight
    -            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
    -                bounds.right += navigationBarHeight
    -            }
    -        }
    -        return bounds
    -    }
    -
    -    /**
    -     * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN]
    -     * and [Build.VERSION_CODES.M], inclusive.
    -     *
    -     *
    -     * Given that multi-window mode isn't supported before N we simply return the real display
    -     * size which should match the window size of a full-screen app.
    -     */
    -    internal fun computeWindowBoundsIceCreamSandwich(activity: Activity): Rect {
    -        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    -        // compatibility with older versions
    -        @Suppress("DEPRECATION")
    -        val defaultDisplay = activity.windowManager.defaultDisplay
    -        val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
    -        val bounds = Rect()
    -        if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
    -            // [Display#getRectSize] is deprecated but we have this for
    -            // compatibility with older versions
    -            @Suppress("DEPRECATION")
    -            defaultDisplay.getRectSize(bounds)
    -        } else {
    -            bounds.right = realDisplaySize.x
    -            bounds.bottom = realDisplaySize.y
    -        }
    -        return bounds
    -    }
    -
    -    /**
    -     * Returns the full (real) size of the display, in pixels, without subtracting any window
    -     * decor or applying any compatibility scale factors.
    -     *
    -     *
    -     * The size is adjusted based on the current rotation of the display.
    -     *
    -     * @return a point representing the real display size in pixels.
    -     *
    -     * @see Display.getRealSize
    -     */
    -    @VisibleForTesting
    -    @Suppress("DEPRECATION")
    -    internal fun getRealSizeForDisplay(display: Display): Point {
    -        val size = Point()
    -        display.getRealSize(size)
    -        return size
    -    }
    -
    -    /**
    -     * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.
    -     *
    -     *
    -     * Note: This is error-prone and is **not** the recommended way to determine the size
    -     * of the overlapping region between the navigation bar and a given window. The best
    -     * approach is to acquire the [android.view.WindowInsets].
    -     */
    -    private fun getNavigationBarHeight(context: Context): Int {
    -        val resources = context.resources
    -        val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
    -        return if (resourceId > 0) {
    -            resources.getDimensionPixelSize(resourceId)
    -        } else 0
    -    }
    -
    -    /**
    -     * Returns the [DisplayCutout] for the given display. Note that display cutout returned
    -     * here is for the display and the insets provided are in the display coordinate system.
    -     *
    -     * @return the display cutout for the given display.
    -     */
    -    @SuppressLint("BanUncheckedReflection")
    -    @RequiresApi(VERSION_CODES.P)
    -    private fun getCutoutForDisplay(display: Display): DisplayCutout? {
    -        var displayCutout: DisplayCutout? = null
    -        try {
    -            val displayInfoClass = Class.forName("android.view.DisplayInfo")
    -            val displayInfoConstructor = displayInfoClass.getConstructor()
    -            displayInfoConstructor.isAccessible = true
    -            val displayInfo = displayInfoConstructor.newInstance()
    -            val getDisplayInfoMethod = display.javaClass.getDeclaredMethod(
    -                "getDisplayInfo", displayInfo.javaClass
    -            )
    -            getDisplayInfoMethod.isAccessible = true
    -            getDisplayInfoMethod.invoke(display, displayInfo)
    -            val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")
    -            displayCutoutField.isAccessible = true
    -            val cutout = displayCutoutField[displayInfo]
    -            if (cutout is DisplayCutout) {
    -                displayCutout = cutout
    -            }
    -        } catch (e: ClassNotFoundException) {
    -            Log.w(TAG, e)
    -        } catch (e: NoSuchMethodException) {
    -            Log.w(TAG, e)
    -        } catch (e: NoSuchFieldException) {
    -            Log.w(TAG, e)
    -        } catch (e: IllegalAccessException) {
    -            Log.w(TAG, e)
    -        } catch (e: InvocationTargetException) {
    -            Log.w(TAG, e)
    -        } catch (e: InstantiationException) {
    -            Log.w(TAG, e)
    -        }
    -        return displayCutout
    +        return WindowMetricsCompatHelper.getInstance()
    +            .maximumWindowMetrics(context, densityCompatHelper)
         }
     
         /**
    @@ -440,18 +81,4 @@
             WindowInsetsCompat.Type.tappableElement(),
             WindowInsetsCompat.Type.displayCutout()
         )
    -
    -    /**
    -     * Computes the current [WindowInsetsCompat] for a given [Context].
    -     */
    -    @RequiresApi(VERSION_CODES.R)
    -    internal fun computeWindowInsetsCompat(@UiContext context: Context): WindowInsetsCompat {
    -        val build = Build.VERSION.SDK_INT
    -        val windowInsetsCompat = if (build >= VERSION_CODES.R) {
    -            currentWindowInsets(context)
    -        } else {
    -            throw Exception("Incompatible SDK version")
    -        }
    -        return windowInsetsCompat
    -    }
     }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
    index 3fb8004..dffafee 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/WindowBackend.kt
    
    @@ -20,6 +20,8 @@
     import androidx.annotation.RestrictTo
     import androidx.annotation.UiContext
     import androidx.core.util.Consumer
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.layout.SupportedPosture
     import androidx.window.layout.WindowLayoutInfo
     import java.util.concurrent.Executor
     
    @@ -50,4 +52,11 @@
         fun hasRegisteredListeners(): Boolean {
             return false
         }
    +
    +    /**
    +     * Returns a [List] of [SupportedPosture] for the device.
    +     * @throws UnsupportedOperationException if the Window SDK version is less than 6.
    +     */
    +    @RequiresWindowSdkExtension(version = 6)
    +    val supportedPostures: List
     }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
    index 13da5ad..76baed2 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackend.kt
    
    @@ -46,7 +46,8 @@
             ): WindowBackend {
                 val safeVendorApiLevel = ExtensionsUtil.safeVendorApiLevel
                 return when {
    -                safeVendorApiLevel >= 2 -> ExtensionWindowBackendApi2(component)
    +                safeVendorApiLevel >= 6 -> ExtensionWindowBackendApi6(component, adapter)
    +                safeVendorApiLevel >= 2 -> ExtensionWindowBackendApi2(component, adapter)
                     safeVendorApiLevel == 1 -> ExtensionWindowBackendApi1(component, adapter)
                     else -> ExtensionWindowBackendApi0()
                 }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
    index 39e2cfe..70b58fe 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi0.kt
    
    @@ -18,6 +18,7 @@
     
     import android.content.Context
     import androidx.core.util.Consumer
    +import androidx.window.layout.SupportedPosture
     import androidx.window.layout.WindowLayoutInfo
     import androidx.window.layout.adapter.WindowBackend
     import java.util.concurrent.Executor
    @@ -35,4 +36,8 @@
         override fun unregisterLayoutChangeCallback(callback: Consumer) {
             // empty implementation since there are no consumers
         }
    +
    +    override val supportedPostures: List
    +        get() = throw UnsupportedOperationException(
    +            "supportedPostures is only supported on Window SDK 6.")
     }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
    index afb37df..947bdc9 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi1.kt
    
    @@ -25,14 +25,15 @@
     import androidx.window.core.ConsumerAdapter
     import androidx.window.extensions.layout.WindowLayoutComponent
     import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
    +import androidx.window.layout.SupportedPosture
     import androidx.window.layout.WindowLayoutInfo
     import androidx.window.layout.adapter.WindowBackend
     import java.util.concurrent.Executor
     import java.util.concurrent.locks.ReentrantLock
     import kotlin.concurrent.withLock
     
    -internal class ExtensionWindowBackendApi1(
    -    private val component: WindowLayoutComponent,
    +internal open class ExtensionWindowBackendApi1(
    +    val component: WindowLayoutComponent,
         private val consumerAdapter: ConsumerAdapter
     ) : WindowBackend {
     
    @@ -121,4 +122,7 @@
             return !(contextToListeners.isEmpty() && listenerToContext.isEmpty() &&
                 consumerToToken.isEmpty())
         }
    +
    +    override val supportedPostures: List
    +        get() = throw UnsupportedOperationException("Extensions version must be at least 6")
     }
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
    index 996f60a..3467e46 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi2.kt
    
    @@ -22,16 +22,17 @@
     import androidx.annotation.UiContext
     import androidx.annotation.VisibleForTesting
     import androidx.core.util.Consumer
    +import androidx.window.core.ConsumerAdapter
     import androidx.window.extensions.layout.WindowLayoutComponent
     import androidx.window.layout.WindowLayoutInfo
    -import androidx.window.layout.adapter.WindowBackend
     import java.util.concurrent.Executor
     import java.util.concurrent.locks.ReentrantLock
     import kotlin.concurrent.withLock
     
    -internal class ExtensionWindowBackendApi2(
    -    private val component: WindowLayoutComponent
    -) : WindowBackend {
    +internal open class ExtensionWindowBackendApi2(
    +    component: WindowLayoutComponent,
    +    adapter: ConsumerAdapter
    +) : ExtensionWindowBackendApi1(component, adapter) {
     
         private val globalLock = ReentrantLock()
     
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt
    new file mode 100644
    index 0000000..ee9ff3c
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowBackendApi6.kt
    
    @@ -0,0 +1,31 @@
    +/*
    + * 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 androidx.window.layout.adapter.extensions
    +
    +import androidx.window.RequiresWindowSdkExtension
    +import androidx.window.core.ConsumerAdapter
    +import androidx.window.extensions.layout.WindowLayoutComponent
    +import androidx.window.layout.SupportedPosture
    +
    +@RequiresWindowSdkExtension(version = 6)
    +internal class ExtensionWindowBackendApi6(
    +    component: WindowLayoutComponent,
    +    adapter: ConsumerAdapter
    +) : ExtensionWindowBackendApi2(component, adapter) {
    +    override val supportedPostures: List
    +        get() = ExtensionsWindowLayoutInfoAdapter.translate(component.supportedWindowFeatures)
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
    index 1c5fb13..df2aa4e 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionsWindowLayoutInfoAdapter.kt
    
    @@ -21,7 +21,9 @@
     import android.os.Build
     import androidx.annotation.UiContext
     import androidx.window.core.Bounds
    +import androidx.window.extensions.layout.DisplayFoldFeature
     import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
    +import androidx.window.extensions.layout.SupportedWindowFeatures
     import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
     import androidx.window.layout.FoldingFeature
     import androidx.window.layout.FoldingFeature.State.Companion.FLAT
    @@ -29,9 +31,10 @@
     import androidx.window.layout.HardwareFoldingFeature
     import androidx.window.layout.HardwareFoldingFeature.Type.Companion.FOLD
     import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
    +import androidx.window.layout.SupportedPosture
     import androidx.window.layout.WindowLayoutInfo
     import androidx.window.layout.WindowMetrics
    -import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
    +import androidx.window.layout.WindowMetricsCalculatorCompat
     
     internal object ExtensionsWindowLayoutInfoAdapter {
     
    @@ -61,10 +64,11 @@
             @UiContext context: Context,
             info: OEMWindowLayoutInfo,
         ): WindowLayoutInfo {
    +        val calculator = WindowMetricsCalculatorCompat()
             return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    -            translate(computeCurrentWindowMetrics(context), info)
    +            translate(calculator.computeCurrentWindowMetrics(context), info)
             } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (context is Activity)) {
    -            translate(computeCurrentWindowMetrics(context), info)
    +            translate(calculator.computeCurrentWindowMetrics(context), info)
             } else {
                 throw UnsupportedOperationException(
                     "Display Features are only supported after Q. Display features for non-Activity " +
    @@ -86,6 +90,17 @@
             return WindowLayoutInfo(features)
         }
     
    +    internal fun translate(features: SupportedWindowFeatures): List {
    +        val isTableTopSupported = features.displayFoldFeatures.any { feature ->
    +            feature.hasProperties(DisplayFoldFeature.FOLD_PROPERTY_SUPPORTS_HALF_OPENED)
    +        }
    +        return if (isTableTopSupported) {
    +            listOf(SupportedPosture.TABLETOP)
    +        } else {
    +            emptyList()
    +        }
    +    }
    +
         /**
          * Checks the bounds for a [FoldingFeature] within a given [WindowMetrics]. Validates the
          * following:
    
    diff --git a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
    index 325c701..86247b3 100644
    --- a/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
    +++ b/window/window/src/main/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackend.kt
    
    @@ -23,6 +23,7 @@
     import androidx.annotation.VisibleForTesting
     import androidx.core.util.Consumer
     import androidx.window.core.Version
    +import androidx.window.layout.SupportedPosture
     import androidx.window.layout.WindowLayoutInfo
     import androidx.window.layout.adapter.WindowBackend
     import androidx.window.layout.adapter.sidecar.ExtensionInterfaceCompat.ExtensionCallbackInterface
    @@ -126,6 +127,9 @@
             }
         }
     
    +    override val supportedPostures: List
    +        get() = throw UnsupportedOperationException("Must be called from extensions.")
    +
         /**
          * Checks if there are no more registered callbacks left for the activity and inform
          * extension if needed.
    
    diff --git a/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
    new file mode 100644
    index 0000000..e743f4c
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/layout/util/BoundsHelper.kt
    
    @@ -0,0 +1,381 @@
    +/*
    + * 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 androidx.window.layout.util
    +
    +import android.annotation.SuppressLint
    +import android.app.Activity
    +import android.content.Context
    +import android.content.res.Configuration
    +import android.graphics.Point
    +import android.graphics.Rect
    +import android.os.Build
    +import android.util.Log
    +import android.view.Display
    +import android.view.DisplayCutout
    +import android.view.WindowManager
    +import androidx.annotation.RequiresApi
    +import androidx.annotation.UiContext
    +import androidx.window.layout.util.ActivityCompatHelperApi24.isInMultiWindowMode
    +import androidx.window.layout.util.BoundsHelper.Companion.TAG
    +import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
    +import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
    +import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetRight
    +import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetTop
    +import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
    +import java.lang.reflect.InvocationTargetException
    +
    +/**
    + * Provides compatibility behavior for calculating bounds of an activity.
    + */
    +internal interface BoundsHelper {
    +
    +    /**
    +     * Compute the current bounds for the given [Activity].
    +     */
    +    fun currentWindowBounds(activity: Activity): Rect
    +
    +    fun maximumWindowBounds(@UiContext context: Context): Rect
    +
    +    companion object {
    +
    +        val TAG: String = BoundsHelper::class.java.simpleName
    +
    +        fun getInstance(): BoundsHelper {
    +            return when {
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
    +                    BoundsHelperApi30Impl
    +                }
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
    +                    BoundsHelperApi29Impl
    +                }
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
    +                    BoundsHelperApi28Impl
    +                }
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
    +                    BoundsHelperApi24Impl
    +                }
    +                else -> {
    +                    BoundsHelperApi16Impl
    +                }
    +            }
    +        }
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.R)
    +private object BoundsHelperApi30Impl : BoundsHelper {
    +    override fun currentWindowBounds(activity: Activity): Rect {
    +        val wm = activity.getSystemService(WindowManager::class.java)
    +        return wm.currentWindowMetrics.bounds
    +    }
    +
    +    override fun maximumWindowBounds(@UiContext context: Context): Rect {
    +        val wm = context.getSystemService(WindowManager::class.java)
    +        return wm.maximumWindowMetrics.bounds
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.Q)
    +private object BoundsHelperApi29Impl : BoundsHelper {
    +
    +    /**
    +     * Computes the window bounds for [Build.VERSION_CODES.Q].
    +     */
    +    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
    +    override fun currentWindowBounds(activity: Activity): Rect {
    +        var bounds: Rect
    +        val config = activity.resources.configuration
    +        try {
    +            val windowConfigField =
    +                Configuration::class.java.getDeclaredField("windowConfiguration")
    +            windowConfigField.isAccessible = true
    +            val windowConfig = windowConfigField[config]
    +            val getBoundsMethod = windowConfig.javaClass.getDeclaredMethod("getBounds")
    +            bounds = Rect(getBoundsMethod.invoke(windowConfig) as Rect)
    +        } catch (e: Exception) {
    +            when (e) {
    +                is NoSuchFieldException,
    +                is NoSuchMethodException,
    +                is IllegalAccessException,
    +                is InvocationTargetException -> {
    +                    Log.w(TAG, e)
    +                    // If reflection fails for some reason default to the P implementation which
    +                    // still has the ability to account for display cutouts.
    +                    bounds = BoundsHelperApi28Impl.currentWindowBounds(activity)
    +                }
    +                else -> throw e
    +            }
    +        }
    +        return bounds
    +    }
    +
    +    override fun maximumWindowBounds(@UiContext context: Context): Rect {
    +        return BoundsHelperApi28Impl.maximumWindowBounds(context)
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.P)
    +private object BoundsHelperApi28Impl : BoundsHelper {
    +
    +    /**
    +     * Computes the window bounds for [Build.VERSION_CODES.P].
    +     *
    +     * NOTE: This method may result in incorrect values if the [android.content.res.Resources]
    +     * value stored at 'navigation_bar_height' does not match the true navigation bar inset on
    +     * the window.
    +     *
    +     */
    +    @SuppressLint("BanUncheckedReflection", "BlockedPrivateApi")
    +    override fun currentWindowBounds(activity: Activity): Rect {
    +        val bounds = Rect()
    +        val config = activity.resources.configuration
    +        try {
    +            val windowConfigField =
    +                Configuration::class.java.getDeclaredField("windowConfiguration")
    +            windowConfigField.isAccessible = true
    +            val windowConfig = windowConfigField[config]
    +
    +            // In multi-window mode we'll use the WindowConfiguration#mBounds property which
    +            // should match the window size. Otherwise we'll use the mAppBounds property and
    +            // will adjust it below.
    +            if (isInMultiWindowMode(activity)) {
    +                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getBounds")
    +                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
    +            } else {
    +                val getAppBounds = windowConfig.javaClass.getDeclaredMethod("getAppBounds")
    +                bounds.set((getAppBounds.invoke(windowConfig) as Rect))
    +            }
    +        } catch (e: Exception) {
    +            when (e) {
    +                is NoSuchFieldException,
    +                is NoSuchMethodException,
    +                is IllegalAccessException,
    +                is InvocationTargetException -> {
    +                    Log.w(TAG, e)
    +                    getRectSizeFromDisplay(activity, bounds)
    +                }
    +                else -> throw e
    +            }
    +        }
    +
    +        val platformWindowManager = activity.windowManager
    +
    +        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    +        // compatibility with older versions
    +        @Suppress("DEPRECATION")
    +        val currentDisplay = platformWindowManager.defaultDisplay
    +        val realDisplaySize = Point()
    +        @Suppress("DEPRECATION")
    +        currentDisplay.getRealSize(realDisplaySize)
    +
    +        if (!isInMultiWindowMode(activity)) {
    +            // The activity is not in multi-window mode. Check if the addition of the
    +            // navigation bar size to mAppBounds results in the real display size and if so
    +            // assume the nav bar height should be added to the result.
    +            val navigationBarHeight = getNavigationBarHeight(activity)
    +            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
    +                bounds.bottom += navigationBarHeight
    +            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
    +                bounds.right += navigationBarHeight
    +            } else if (bounds.left == navigationBarHeight) {
    +                bounds.left = 0
    +            }
    +        }
    +        if ((bounds.width() < realDisplaySize.x || bounds.height() < realDisplaySize.y) &&
    +            !isInMultiWindowMode(activity)
    +        ) {
    +            // If the corrected bounds are not the same as the display size and the activity is
    +            // not in multi-window mode it is possible there are unreported cutouts inset-ing
    +            // the window depending on the layoutInCutoutMode. Check for them here by getting
    +            // the cutout from the display itself.
    +            val displayCutout = getCutoutForDisplay(currentDisplay)
    +            if (displayCutout != null) {
    +                if (bounds.left == safeInsetLeft(displayCutout)) {
    +                    bounds.left = 0
    +                }
    +                if (realDisplaySize.x - bounds.right == safeInsetRight(displayCutout)) {
    +                    bounds.right += safeInsetRight(displayCutout)
    +                }
    +                if (bounds.top == safeInsetTop(displayCutout)) {
    +                    bounds.top = 0
    +                }
    +                if (realDisplaySize.y - bounds.bottom == safeInsetBottom(displayCutout)) {
    +                    bounds.bottom += safeInsetBottom(displayCutout)
    +                }
    +            }
    +        }
    +        return bounds
    +    }
    +
    +    override fun maximumWindowBounds(@UiContext context: Context): Rect {
    +        return BoundsHelperApi24Impl.maximumWindowBounds(context)
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.N)
    +private object BoundsHelperApi24Impl : BoundsHelper {
    +
    +    /**
    +     * Computes the window bounds for platforms between [Build.VERSION_CODES.N]
    +     * and [Build.VERSION_CODES.O_MR1], inclusive.
    +     *
    +     * NOTE: This method may result in incorrect values under the following conditions:
    +     *
    +     *  * If the activity is in multi-window mode the origin of the returned bounds will
    +     *  always be anchored at (0, 0).
    +     *  * If the [android.content.res.Resources] value stored at 'navigation_bar_height' does
    +     *  not match the true navigation bar size the returned bounds will not take into account
    +     *  the navigation bar.
    +     *
    +     */
    +    override fun currentWindowBounds(activity: Activity): Rect {
    +        val bounds = Rect()
    +        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    +        // compatibility with older versions
    +        @Suppress("DEPRECATION")
    +        val defaultDisplay = activity.windowManager.defaultDisplay
    +        // [Display#getRectSize] is deprecated but we have this for
    +        // compatibility with older versions
    +        @Suppress("DEPRECATION")
    +        defaultDisplay.getRectSize(bounds)
    +        if (!isInMultiWindowMode(activity)) {
    +            // The activity is not in multi-window mode. Check if the addition of the
    +            // navigation bar size to Display#getSize() results in the real display size and
    +            // if so return this value. If not, return the result of Display#getSize().
    +            val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
    +            val navigationBarHeight = getNavigationBarHeight(activity)
    +            if (bounds.bottom + navigationBarHeight == realDisplaySize.y) {
    +                bounds.bottom += navigationBarHeight
    +            } else if (bounds.right + navigationBarHeight == realDisplaySize.x) {
    +                bounds.right += navigationBarHeight
    +            }
    +        }
    +        return bounds
    +    }
    +
    +    override fun maximumWindowBounds(@UiContext context: Context): Rect {
    +        return BoundsHelperApi16Impl.maximumWindowBounds(context)
    +    }
    +}
    +
    +private object BoundsHelperApi16Impl : BoundsHelper {
    +
    +    /**
    +     * Computes the window bounds for platforms between [Build.VERSION_CODES.JELLY_BEAN]
    +     * and [Build.VERSION_CODES.M], inclusive.
    +     *
    +     * Given that multi-window mode isn't supported before N we simply return the real display
    +     * size which should match the window size of a full-screen app.
    +     */
    +    override fun currentWindowBounds(activity: Activity): Rect {
    +        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    +        // compatibility with older versions
    +        @Suppress("DEPRECATION")
    +        val defaultDisplay = activity.windowManager.defaultDisplay
    +        val realDisplaySize = getRealSizeForDisplay(defaultDisplay)
    +        val bounds = Rect()
    +        if (realDisplaySize.x == 0 || realDisplaySize.y == 0) {
    +            // [Display#getRectSize] is deprecated but we have this for
    +            // compatibility with older versions
    +            @Suppress("DEPRECATION")
    +            defaultDisplay.getRectSize(bounds)
    +        } else {
    +            bounds.right = realDisplaySize.x
    +            bounds.bottom = realDisplaySize.y
    +        }
    +        return bounds
    +    }
    +
    +    override fun maximumWindowBounds(@UiContext context: Context): Rect {
    +        val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    +        // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    +        // compatibility with older versions, as we can't reliably get the display associated
    +        // with a Context through public APIs either.
    +        @Suppress("DEPRECATION")
    +        val display = wm.defaultDisplay
    +        val displaySize = getRealSizeForDisplay(display)
    +        return Rect(0, 0, displaySize.x, displaySize.y)
    +    }
    +}
    +
    +/**
    + * Returns the [android.content.res.Resources] value stored as 'navigation_bar_height'.
    + *
    + * Note: This is error-prone and is **not** the recommended way to determine the size
    + * of the overlapping region between the navigation bar and a given window. The best
    + * approach is to acquire the [android.view.WindowInsets].
    + */
    +private fun getNavigationBarHeight(context: Context): Int {
    +    val resources = context.resources
    +    val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
    +    return if (resourceId > 0) {
    +        resources.getDimensionPixelSize(resourceId)
    +    } else 0
    +}
    +
    +private fun getRectSizeFromDisplay(activity: Activity, bounds: Rect) {
    +    // [WindowManager#getDefaultDisplay] is deprecated but we have this for
    +    // compatibility with older versions
    +    @Suppress("DEPRECATION")
    +    val defaultDisplay = activity.windowManager.defaultDisplay
    +    // [Display#getRectSize] is deprecated but we have this for
    +    // compatibility with older versions
    +    @Suppress("DEPRECATION")
    +    defaultDisplay.getRectSize(bounds)
    +}
    +
    +/**
    + * Returns the [DisplayCutout] for the given display. Note that display cutout returned
    + * here is for the display and the insets provided are in the display coordinate system.
    + *
    + * @return the display cutout for the given display.
    + */
    +@SuppressLint("BanUncheckedReflection")
    +@RequiresApi(Build.VERSION_CODES.P)
    +private fun getCutoutForDisplay(display: Display): DisplayCutout? {
    +    var displayCutout: DisplayCutout? = null
    +    try {
    +        val displayInfoClass = Class.forName("android.view.DisplayInfo")
    +        val displayInfoConstructor = displayInfoClass.getConstructor()
    +        displayInfoConstructor.isAccessible = true
    +        val displayInfo = displayInfoConstructor.newInstance()
    +        val getDisplayInfoMethod = display.javaClass.getDeclaredMethod("getDisplayInfo",
    +            displayInfo.javaClass
    +        )
    +        getDisplayInfoMethod.isAccessible = true
    +        getDisplayInfoMethod.invoke(display, displayInfo)
    +        val displayCutoutField = displayInfo.javaClass.getDeclaredField("displayCutout")
    +        displayCutoutField.isAccessible = true
    +        val cutout = displayCutoutField[displayInfo]
    +        if (cutout is DisplayCutout) {
    +            displayCutout = cutout
    +        }
    +    } catch (e: Exception) {
    +        when (e) {
    +            is ClassNotFoundException,
    +            is NoSuchMethodException,
    +            is NoSuchFieldException,
    +            is IllegalAccessException,
    +            is InvocationTargetException,
    +            is InstantiationException -> {
    +                Log.w(TAG, e)
    +            }
    +            else -> throw e
    +        }
    +    }
    +    return displayCutout
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
    index 2b5af8a..51c136a 100644
    --- a/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
    +++ b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
    
    @@ -19,7 +19,6 @@
     import android.app.Activity
     import android.content.Context
     import android.content.ContextWrapper
    -import android.graphics.Rect
     import android.inputmethodservice.InputMethodService
     import android.os.Build
     import android.view.WindowManager
    @@ -27,7 +26,6 @@
     import androidx.annotation.RequiresApi
     import androidx.annotation.UiContext
     import androidx.core.view.WindowInsetsCompat
    -import androidx.window.layout.WindowMetrics
     
     internal object ContextCompatHelper {
         /**
    @@ -60,32 +58,9 @@
         }
     }
     
    -@RequiresApi(Build.VERSION_CODES.N)
    -internal object ContextCompatHelperApi24 {
    -    fun isInMultiWindowMode(activity: Activity): Boolean {
    -        return activity.isInMultiWindowMode
    -    }
    -}
    -
     @RequiresApi(Build.VERSION_CODES.R)
     internal object ContextCompatHelperApi30 {
     
    -    fun currentWindowMetrics(@UiContext context: Context): WindowMetrics {
    -        val wm = context.getSystemService(WindowManager::class.java)
    -        val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
    -        return WindowMetrics(wm.currentWindowMetrics.bounds, insets)
    -    }
    -
    -    fun currentWindowBounds(@UiContext context: Context): Rect {
    -        val wm = context.getSystemService(WindowManager::class.java)
    -        return wm.currentWindowMetrics.bounds
    -    }
    -
    -    fun maximumWindowBounds(@UiContext context: Context): Rect {
    -        val wm = context.getSystemService(WindowManager::class.java)
    -        return wm.maximumWindowMetrics.bounds
    -    }
    -
         /**
          * Computes the [WindowInsetsCompat] for platforms above [Build.VERSION_CODES.R], inclusive.
          * @DoNotInline required for implementation-specific class method to prevent it from being
    
    diff --git a/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt
    new file mode 100644
    index 0000000..15ce04e
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/layout/util/DensityCompatHelper.kt
    
    @@ -0,0 +1,76 @@
    +/*
    + * 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 androidx.window.layout.util
    +
    +import android.content.Context
    +import android.content.res.Configuration
    +import android.os.Build
    +import android.util.DisplayMetrics
    +import android.view.WindowManager
    +import android.view.WindowMetrics as AndroidWindowMetrics
    +import androidx.annotation.RequiresApi
    +import androidx.annotation.UiContext
    +
    +/**
    + * Provides compatibility behavior for functionality related to display density.
    + */
    +internal interface DensityCompatHelper {
    +
    +    /**
    +     * Returns the logical density of the display associated with the [Context].
    +     */
    +    fun density(context: Context): Float
    +
    +    /**
    +     * Returns the logical density of the display associated with the [Configuration] or
    +     * [AndroidWindowMetrics], depending on the SDK level.
    +     */
    +    fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float
    +
    +    companion object {
    +        fun getInstance(): DensityCompatHelper {
    +            return when {
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->
    +                    DensityCompatHelperApi34Impl
    +                else ->
    +                    DensityCompatHelperBaseImpl
    +            }
    +        }
    +    }
    +}
    +
    +private object DensityCompatHelperBaseImpl : DensityCompatHelper {
    +    override fun density(context: Context): Float {
    +        return context.resources.displayMetrics.density
    +    }
    +
    +    override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {
    +        return configuration.densityDpi.toFloat() /
    +            DisplayMetrics.DENSITY_DEFAULT
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +private object DensityCompatHelperApi34Impl : DensityCompatHelper {
    +    override fun density(@UiContext context: Context): Float {
    +        return context.getSystemService(WindowManager::class.java).currentWindowMetrics.density
    +    }
    +
    +    override fun density(configuration: Configuration, windowMetrics: AndroidWindowMetrics): Float {
    +        return windowMetrics.density
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
    new file mode 100644
    index 0000000..ae46dd1
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/layout/util/DisplayHelper.kt
    
    @@ -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 androidx.window.layout.util
    +
    +import android.graphics.Point
    +import android.view.Display
    +
    +internal object DisplayHelper {
    +
    +    /**
    +     * Returns the full (real) size of the display, in pixels, without subtracting any window
    +     * decor or applying any compatibility scale factors.
    +     *
    +     * The size is adjusted based on the current rotation of the display.
    +     *
    +     * @return a point representing the real display size in pixels.
    +     *
    +     * @see Display.getRealSize
    +     */
    +    @Suppress("DEPRECATION")
    +    fun getRealSizeForDisplay(display: Display): Point {
    +        val size = Point()
    +        display.getRealSize(size)
    +        return size
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt
    new file mode 100644
    index 0000000..bb22b7b
    --- /dev/null
    +++ b/window/window/src/main/java/androidx/window/layout/util/WindowMetricsCompatHelper.kt
    
    @@ -0,0 +1,238 @@
    +/*
    + * 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 androidx.window.layout.util
    +
    +import android.app.Activity
    +import android.content.Context
    +import android.graphics.Rect
    +import android.inputmethodservice.InputMethodService
    +import android.os.Build
    +import android.view.WindowManager
    +import android.view.WindowMetrics as AndroidWindowMetrics
    +import androidx.annotation.RequiresApi
    +import androidx.annotation.UiContext
    +import androidx.core.view.WindowInsetsCompat
    +import androidx.window.core.Bounds
    +import androidx.window.layout.WindowMetrics
    +import androidx.window.layout.WindowMetricsCalculator
    +import androidx.window.layout.util.ContextCompatHelper.unwrapUiContext
    +import androidx.window.layout.util.ContextCompatHelperApi30.currentWindowInsets
    +import androidx.window.layout.util.DisplayHelper.getRealSizeForDisplay
    +
    +/**
    + * Provides compatibility behavior for functionality related to [WindowMetricsCalculator].
    + */
    +internal interface WindowMetricsCompatHelper {
    +
    +    /**
    +     * Translates platform [AndroidWindowMetrics] to jetpack [WindowMetrics].
    +     */
    +    @RequiresApi(Build.VERSION_CODES.R)
    +    fun translateWindowMetrics(windowMetrics: AndroidWindowMetrics, density: Float): WindowMetrics
    +
    +    /**
    +     * Returns the [WindowMetrics] associated with the provided [Context].
    +     */
    +    fun currentWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics
    +
    +    /**
    +     * Returns the [WindowMetrics] associated with the provided [Activity].
    +     */
    +    fun currentWindowMetrics(
    +        activity: Activity,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics
    +
    +    /**
    +     * Returns the maximum [WindowMetrics] for a given [UiContext].
    +     */
    +    fun maximumWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics
    +
    +    companion object {
    +        fun getInstance(): WindowMetricsCompatHelper {
    +            return when {
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE ->
    +                    WindowMetricsCompatHelperApi34Impl
    +                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
    +                    WindowMetricsCompatHelperApi30Impl
    +                else ->
    +                    WindowMetricsCompatHelperBaseImpl
    +            }
    +        }
    +    }
    +}
    +
    +internal object WindowMetricsCompatHelperBaseImpl : WindowMetricsCompatHelper {
    +
    +    override fun translateWindowMetrics(
    +        windowMetrics: AndroidWindowMetrics,
    +        density: Float
    +    ): WindowMetrics {
    +        throw UnsupportedOperationException("translateWindowMetrics not available before API30")
    +    }
    +
    +    override fun currentWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        when (val unwrappedContext = unwrapUiContext(context)) {
    +            is Activity -> {
    +                return currentWindowMetrics(unwrappedContext, densityCompatHelper)
    +            }
    +
    +            is InputMethodService -> {
    +                val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    +
    +                // On older SDK levels, the app and IME could show up on different displays.
    +                // However, there isn't a way for us to figure this out from the application
    +                // layer. But, this should be good enough for now given the small likelihood of
    +                // IMEs showing up on non-primary displays on these SDK levels.
    +                @Suppress("DEPRECATION")
    +                val displaySize = getRealSizeForDisplay(wm.defaultDisplay)
    +
    +                // IME occupies the whole display bounds.
    +                val imeBounds = Rect(0, 0, displaySize.x, displaySize.y)
    +                return WindowMetrics(
    +                    imeBounds,
    +                    density = densityCompatHelper.density(context)
    +                )
    +            }
    +
    +            else -> {
    +                throw IllegalArgumentException("$context is not a UiContext")
    +            }
    +        }
    +    }
    +
    +    override fun currentWindowMetrics(
    +        activity: Activity,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        // TODO (b/233899790): compute insets for other platform versions below R
    +        return WindowMetrics(
    +            Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),
    +            WindowInsetsCompat.Builder().build(),
    +            densityCompatHelper.density(activity)
    +        )
    +    }
    +
    +    override fun maximumWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        // TODO (b/233899790): compute insets for other platform versions below Rs
    +        return WindowMetrics(
    +            Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),
    +            WindowInsetsCompat.Builder().build(),
    +            densityCompatHelper.density(context)
    +        )
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.R)
    +internal object WindowMetricsCompatHelperApi30Impl : WindowMetricsCompatHelper {
    +
    +    override fun translateWindowMetrics(
    +        windowMetrics: AndroidWindowMetrics,
    +        density: Float
    +    ): WindowMetrics {
    +        return WindowMetrics(
    +            windowMetrics.bounds,
    +            WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),
    +            density
    +        )
    +    }
    +
    +    override fun currentWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        val wm = context.getSystemService(WindowManager::class.java)
    +        val insets = WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
    +        val density = context.resources.displayMetrics.density
    +        return WindowMetrics(wm.currentWindowMetrics.bounds, insets, density)
    +    }
    +
    +    override fun currentWindowMetrics(
    +        activity: Activity,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        return WindowMetrics(
    +            Bounds(BoundsHelper.getInstance().currentWindowBounds(activity)),
    +            currentWindowInsets(activity),
    +            densityCompatHelper.density(activity)
    +        )
    +    }
    +
    +    override fun maximumWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        return WindowMetrics(
    +            Bounds(BoundsHelper.getInstance().maximumWindowBounds(context)),
    +            currentWindowInsets(context),
    +            densityCompatHelper.density(context)
    +        )
    +    }
    +}
    +
    +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    +internal object WindowMetricsCompatHelperApi34Impl : WindowMetricsCompatHelper {
    +
    +    override fun translateWindowMetrics(
    +        windowMetrics: AndroidWindowMetrics,
    +        density: Float
    +    ): WindowMetrics {
    +        return WindowMetrics(
    +            windowMetrics.bounds,
    +            WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets),
    +            windowMetrics.density
    +        )
    +    }
    +
    +    override fun currentWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        val wm = context.getSystemService(WindowManager::class.java)
    +        return WindowMetrics(wm.currentWindowMetrics.bounds,
    +            WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets),
    +            wm.currentWindowMetrics.density)
    +    }
    +
    +    override fun currentWindowMetrics(
    +        activity: Activity,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        return WindowMetricsCompatHelperApi30Impl
    +            .currentWindowMetrics(activity, densityCompatHelper)
    +    }
    +
    +    override fun maximumWindowMetrics(
    +        @UiContext context: Context,
    +        densityCompatHelper: DensityCompatHelper
    +    ): WindowMetrics {
    +        return WindowMetricsCompatHelperApi30Impl
    +            .maximumWindowMetrics(context, densityCompatHelper)
    +    }
    +}
    
    diff --git a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
    index 48c03e6..d785a25 100644
    --- a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
    +++ b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
    
    @@ -18,6 +18,7 @@
     
     import android.util.Log
     import java.lang.reflect.Constructor
    +import java.lang.reflect.Field
     import java.lang.reflect.Method
     import java.lang.reflect.Modifier
     import kotlin.reflect.KClass
    @@ -44,10 +45,10 @@
          * Otherwise will return the validation result from the [block]
          */
         @JvmStatic
    -    internal fun validateReflection(errorMessage: String? = null, block: () -> Boolean): Boolean {
    +    internal fun validateReflection(errorMessage: String, block: () -> Boolean): Boolean {
             return try {
                 val result = block()
    -            if (!result && errorMessage != null) {
    +            if (!result) {
                     Log.e("ReflectionGuard", errorMessage)
                 }
                 result
    @@ -57,6 +58,9 @@
             } catch (noMethod: NoSuchMethodException) {
                 Log.e("ReflectionGuard", "NoSuchMethod: ${errorMessage.orEmpty()}")
                 false
    +        } catch (noField: NoSuchFieldException) {
    +            Log.e("ReflectionGuard", "NoSuchField: ${errorMessage.orEmpty()}")
    +            false
             }
         }
     
    @@ -77,6 +81,14 @@
             }
     
         /**
    +     * Checks if a field has public modifier
    +     */
    +    internal val Field.isPublic: Boolean
    +        get() {
    +            return Modifier.isPublic(modifiers)
    +        }
    +
    +    /**
          * Checks if a method's return value is type of kotlin [clazz]
          */
         internal fun Method.doesReturn(clazz: KClass<*>): Boolean {
    
    diff --git a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
    index b86fdc1..5443c9d 100644
    --- a/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
    +++ b/window/window/src/main/java/androidx/window/reflection/WindowExtensionsConstants.kt
    
    @@ -38,19 +38,33 @@
         internal const val WINDOW_EXTENSIONS_CLASS =
             "$WINDOW_EXTENSIONS_PACKAGE_NAME.WindowExtensions"
     
    +    internal const val LAYOUT_PACKAGE = "layout"
    +
         /**
          * Constant name for class [androidx.window.extensions.layout.FoldingFeature]
          * used for reflection
          */
         internal const val FOLDING_FEATURE_CLASS =
    -        "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.FoldingFeature"
    +        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.FoldingFeature"
    +
    +    /**
    +     * Constant name for class [androidx.window.extensions.layout.SupportedWindowFeatures]
    +     */
    +    internal const val SUPPORTED_WINDOW_FEATURES_CLASS =
    +        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.SupportedWindowFeatures"
    +
    +    /**
    +     * Constant name for class [androidx.window.extensions.layout.DisplayFoldFeature]
    +     */
    +    internal const val DISPLAY_FOLD_FEATURE_CLASS =
    +        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.DisplayFoldFeature"
     
         /**
          * Constant name for class [androidx.window.extensions.layout.WindowLayoutComponent]
          * used for reflection
          */
         internal const val WINDOW_LAYOUT_COMPONENT_CLASS =
    -        "$WINDOW_EXTENSIONS_PACKAGE_NAME.layout.WindowLayoutComponent"
    +        "$WINDOW_EXTENSIONS_PACKAGE_NAME.$LAYOUT_PACKAGE.WindowLayoutComponent"
     
         /**
          * Constant name for class [androidx.window.extensions.area.WindowAreaComponent]
    
    diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
    index 5342612..a2880ca 100644
    --- a/window/window/src/main/res/values/attrs.xml
    +++ b/window/window/src/main/res/values/attrs.xml
    
    @@ -43,7 +43,7 @@
              `ActivityRule`. The suggested usage is to set the tag to be able to differentiate between
              different rules in the callbacks.
              For example, it can be used to compute the right `SplitAttributes` for the given split rule
    -         in `SplitAttributes` calculator function. -->
    +         in `SplitAttributesCalculator.computeSplitAttributesForParams`. -->