Merge "StreamUseCase new implementation in camera-camera2" into androidx-main am: 33ba2beed7

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

Change-Id: I7b8c35140db751d4cba212ec263bb63848a041d2
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/appcompat/appcompat-resources/api/api_lint.ignore b/appcompat/appcompat-resources/api/api_lint.ignore
index 0cfa261..dd0cf8d 100644
--- a/appcompat/appcompat-resources/api/api_lint.ignore
+++ b/appcompat/appcompat-resources/api/api_lint.ignore
@@ -17,6 +17,8 @@
     Missing nullability on parameter `tint` in method `setTintList`
 MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#DrawableWrapperCompat(android.graphics.drawable.Drawable) parameter #0:
     Missing nullability on parameter `drawable` in method `DrawableWrapperCompat`
+MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#getCurrent():
     Missing nullability on method `getCurrent` return
 MissingNullability: androidx.appcompat.graphics.drawable.DrawableWrapperCompat#getPadding(android.graphics.Rect) parameter #0:
diff --git a/appcompat/appcompat/api/api_lint.ignore b/appcompat/appcompat/api/api_lint.ignore
index ff1d34f..90541cfd 100644
--- a/appcompat/appcompat/api/api_lint.ignore
+++ b/appcompat/appcompat/api/api_lint.ignore
@@ -91,12 +91,8 @@
     Invalid nullability on parameter `filters` in method `setFilters`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.appcompat.widget.AppCompatToggleButton#setFilters(android.text.InputFilter[]) parameter #0:
     Invalid nullability on parameter `filters` in method `setFilters`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.appcompat.widget.LinearLayoutCompat#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.appcompat.widget.ListPopupWindow#getListView():
     Invalid nullability on method `getListView` return. Overrides of unannotated super method cannot be Nullable.
-InvalidNullabilityOverride: androidx.appcompat.widget.SwitchCompat#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `c` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.appcompat.widget.SwitchCompat#getCustomSelectionActionModeCallback():
     Invalid nullability on method `getCustomSelectionActionModeCallback` return. Overrides of unannotated super method cannot be Nullable.
 InvalidNullabilityOverride: androidx.appcompat.widget.SwitchCompat#setFilters(android.text.InputFilter[]) parameter #0:
@@ -555,6 +551,8 @@
     Missing nullability on parameter `attrs` in method `createView`
 MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#DrawerArrowDrawable(android.content.Context) parameter #0:
     Missing nullability on parameter `context` in method `DrawerArrowDrawable`
+MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#getPaint():
     Missing nullability on method `getPaint` return
 MissingNullability: androidx.appcompat.graphics.drawable.DrawerArrowDrawable#setColorFilter(android.graphics.ColorFilter) parameter #0:
@@ -727,6 +725,8 @@
     Missing nullability on parameter `p` in method `generateLayoutParams`
 MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#getDividerDrawable():
     Missing nullability on method `getDividerDrawable` return
+MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent) parameter #0:
     Missing nullability on parameter `event` in method `onInitializeAccessibilityEvent`
 MissingNullability: androidx.appcompat.widget.LinearLayoutCompat#onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo) parameter #0:
@@ -797,6 +797,8 @@
     Missing nullability on parameter `source` in method `onShareTargetSelected`
 MissingNullability: androidx.appcompat.widget.ShareActionProvider.OnShareTargetSelectedListener#onShareTargetSelected(androidx.appcompat.widget.ShareActionProvider, android.content.Intent) parameter #1:
     Missing nullability on parameter `intent` in method `onShareTargetSelected`
+MissingNullability: androidx.appcompat.widget.SwitchCompat#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `c` in method `draw`
 MissingNullability: androidx.appcompat.widget.SwitchCompat#getTextOff():
     Missing nullability on method `getTextOff` return
 MissingNullability: androidx.appcompat.widget.SwitchCompat#getTextOn():
@@ -831,6 +833,8 @@
     Missing nullability on parameter `thumb` in method `setThumbDrawable`
 MissingNullability: androidx.appcompat.widget.SwitchCompat#setTrackDrawable(android.graphics.drawable.Drawable) parameter #0:
     Missing nullability on parameter `track` in method `setTrackDrawable`
+MissingNullability: androidx.appcompat.widget.SwitchCompat#verifyDrawable(android.graphics.drawable.Drawable) parameter #0:
+    Missing nullability on parameter `who` in method `verifyDrawable`
 MissingNullability: androidx.appcompat.widget.Toolbar#checkLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
     Missing nullability on parameter `p` in method `checkLayoutParams`
 MissingNullability: androidx.appcompat.widget.Toolbar#generateDefaultLayoutParams():
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
index 069c76c..5719451 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright 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.
@@ -20,4 +20,3 @@
  * An activity for locales with a unique class name.
  */
 public class LocalesActivityA extends LocalesUpdateActivity {}
-
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
index 75b3888..0d0ac6e 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaMigrationLocalCtsTest.java
@@ -22,12 +22,10 @@
 import androidx.appsearch.app.AppSearchSession;
 import androidx.appsearch.localstorage.LocalStorage;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.FlakyTest;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-@FlakyTest(bugId = 242761389)
-public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase{
+public class AppSearchSchemaMigrationLocalCtsTest extends AppSearchSchemaMigrationCtsTestBase {
     @Override
     protected ListenableFuture<AppSearchSession> createSearchSessionAsync(@NonNull String dbName) {
         Context context = ApplicationProvider.getApplicationContext();
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
index abc2e63..33a5655 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/SupportConfig.kt
@@ -33,7 +33,7 @@
      * Either an integer value or a pre-release platform code, prefixed with "android-" (ex.
      * "android-28" or "android-Q") as you would see within the SDK's platforms directory.
      */
-    const val COMPILE_SDK_VERSION = "android-33"
+    const val COMPILE_SDK_VERSION = "android-UpsideDownCake"
 
     /**
      * The Android SDK version to use for targetSdkVersion meta-data.
diff --git a/busytown/impl/check_translations.sh b/busytown/impl/check_translations.sh
index 35501ad..0f9e440 100755
--- a/busytown/impl/check_translations.sh
+++ b/busytown/impl/check_translations.sh
@@ -20,6 +20,7 @@
 find . \
     \( \
       -iname '*sample*' \
+      -o -iname '*demo*' \
       -o -iname '*donottranslate*' \
       -o -iname '*debug*' \
       -o -iname '*test*' \
diff --git a/constraintlayout/constraintlayout/api/api_lint.ignore b/constraintlayout/constraintlayout/api/api_lint.ignore
index 1f05e12..422e6ca 100644
--- a/constraintlayout/constraintlayout/api/api_lint.ignore
+++ b/constraintlayout/constraintlayout/api/api_lint.ignore
@@ -217,24 +217,6 @@
     Invalid nullability on parameter `target` in method `onNestedFling`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.constraintlayout.motion.widget.MotionLayout#onNestedPreFling(android.view.View, float, float) parameter #0:
     Invalid nullability on parameter `target` in method `onNestedPreFling`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.ImageFilterButton#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.ImageFilterView#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.MockView#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.MotionButton#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.utils.widget.MotionLabel#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.ConstraintHelper#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.Guideline#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.Placeholder#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.constraintlayout.widget.ReactiveGuide#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 
 
 KotlinOperator: androidx.constraintlayout.motion.utils.ViewTimeCycle#get(float, long, android.view.View, androidx.constraintlayout.core.motion.utils.KeyCache):
@@ -349,6 +331,8 @@
     Missing nullability on method `getSpans` return
 MissingNullability: androidx.constraintlayout.helper.widget.Grid#init(android.util.AttributeSet) parameter #0:
     Missing nullability on parameter `attrs` in method `init`
+MissingNullability: androidx.constraintlayout.helper.widget.Grid#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.constraintlayout.helper.widget.Grid#setColumnWeights(String) parameter #0:
     Missing nullability on parameter `columnWeights` in method `setColumnWeights`
 MissingNullability: androidx.constraintlayout.helper.widget.Grid#setRowWeights(String) parameter #0:
@@ -1089,6 +1073,8 @@
     Missing nullability on parameter `context` in method `ImageFilterButton`
 MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterButton#ImageFilterButton(android.content.Context, android.util.AttributeSet, int) parameter #1:
     Missing nullability on parameter `attrs` in method `ImageFilterButton`
+MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterButton#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterButton#setImageDrawable(android.graphics.drawable.Drawable) parameter #0:
     Missing nullability on parameter `drawable` in method `setImageDrawable`
 MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#ImageFilterView(android.content.Context) parameter #0:
@@ -1101,6 +1087,8 @@
     Missing nullability on parameter `context` in method `ImageFilterView`
 MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#ImageFilterView(android.content.Context, android.util.AttributeSet, int) parameter #1:
     Missing nullability on parameter `attrs` in method `ImageFilterView`
+MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#setAltImageDrawable(android.graphics.drawable.Drawable) parameter #0:
     Missing nullability on parameter `altDrawable` in method `setAltImageDrawable`
 MissingNullability: androidx.constraintlayout.utils.widget.ImageFilterView#setImageDrawable(android.graphics.drawable.Drawable) parameter #0:
@@ -1117,6 +1105,8 @@
     Missing nullability on parameter `attrs` in method `MockView`
 MissingNullability: androidx.constraintlayout.utils.widget.MockView#mText:
     Missing nullability on field `mText` in class `class androidx.constraintlayout.utils.widget.MockView`
+MissingNullability: androidx.constraintlayout.utils.widget.MockView#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#MotionButton(android.content.Context) parameter #0:
     Missing nullability on parameter `context` in method `MotionButton`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#MotionButton(android.content.Context, android.util.AttributeSet) parameter #0:
@@ -1127,6 +1117,8 @@
     Missing nullability on parameter `context` in method `MotionButton`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#MotionButton(android.content.Context, android.util.AttributeSet, int) parameter #1:
     Missing nullability on parameter `attrs` in method `MotionButton`
+MissingNullability: androidx.constraintlayout.utils.widget.MotionButton#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#MotionLabel(android.content.Context) parameter #0:
     Missing nullability on parameter `context` in method `MotionLabel`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#MotionLabel(android.content.Context, android.util.AttributeSet) parameter #0:
@@ -1135,6 +1127,8 @@
     Missing nullability on parameter `context` in method `MotionLabel`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#getTypeface():
     Missing nullability on method `getTypeface` return
+MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#setText(CharSequence) parameter #0:
     Missing nullability on parameter `text` in method `setText`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionLabel#setTypeface(android.graphics.Typeface) parameter #0:
@@ -1149,6 +1143,8 @@
     Missing nullability on parameter `context` in method `MotionTelltales`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionTelltales#MotionTelltales(android.content.Context, android.util.AttributeSet, int) parameter #1:
     Missing nullability on parameter `attrs` in method `MotionTelltales`
+MissingNullability: androidx.constraintlayout.utils.widget.MotionTelltales#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.constraintlayout.utils.widget.MotionTelltales#setText(CharSequence) parameter #0:
     Missing nullability on parameter `text` in method `setText`
 MissingNullability: androidx.constraintlayout.widget.Barrier#Barrier(android.content.Context) parameter #0:
@@ -1267,6 +1263,8 @@
     Missing nullability on field `mReferenceTags` in class `class androidx.constraintlayout.widget.ConstraintHelper`
 MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#myContext:
     Missing nullability on field `myContext` in class `class androidx.constraintlayout.widget.ConstraintHelper`
+MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#removeView(android.view.View) parameter #0:
     Missing nullability on parameter `view` in method `removeView`
 MissingNullability: androidx.constraintlayout.widget.ConstraintHelper#resolveRtl(androidx.constraintlayout.core.widgets.ConstraintWidget, boolean) parameter #0:
@@ -1713,6 +1711,8 @@
     Missing nullability on parameter `context` in method `Guideline`
 MissingNullability: androidx.constraintlayout.widget.Guideline#Guideline(android.content.Context, android.util.AttributeSet, int, int) parameter #1:
     Missing nullability on parameter `attrs` in method `Guideline`
+MissingNullability: androidx.constraintlayout.widget.Guideline#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.constraintlayout.widget.Placeholder#Placeholder(android.content.Context) parameter #0:
     Missing nullability on parameter `context` in method `Placeholder`
 MissingNullability: androidx.constraintlayout.widget.Placeholder#Placeholder(android.content.Context, android.util.AttributeSet) parameter #0:
@@ -1729,6 +1729,8 @@
     Missing nullability on parameter `attrs` in method `Placeholder`
 MissingNullability: androidx.constraintlayout.widget.Placeholder#getContent():
     Missing nullability on method `getContent` return
+MissingNullability: androidx.constraintlayout.widget.Placeholder#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.constraintlayout.widget.Placeholder#updatePostMeasure(androidx.constraintlayout.widget.ConstraintLayout) parameter #0:
     Missing nullability on parameter `container` in method `updatePostMeasure`
 MissingNullability: androidx.constraintlayout.widget.Placeholder#updatePreLayout(androidx.constraintlayout.widget.ConstraintLayout) parameter #0:
@@ -1747,6 +1749,8 @@
     Missing nullability on parameter `context` in method `ReactiveGuide`
 MissingNullability: androidx.constraintlayout.widget.ReactiveGuide#ReactiveGuide(android.content.Context, android.util.AttributeSet, int, int) parameter #1:
     Missing nullability on parameter `attrs` in method `ReactiveGuide`
+MissingNullability: androidx.constraintlayout.widget.ReactiveGuide#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.constraintlayout.widget.SharedValues#addListener(int, androidx.constraintlayout.widget.SharedValues.SharedValuesListener) parameter #1:
     Missing nullability on parameter `listener` in method `addListener`
 MissingNullability: androidx.constraintlayout.widget.SharedValues#removeListener(androidx.constraintlayout.widget.SharedValues.SharedValuesListener) parameter #0:
diff --git a/coordinatorlayout/coordinatorlayout/api/api_lint.ignore b/coordinatorlayout/coordinatorlayout/api/api_lint.ignore
index f200680..06d3c6e 100644
--- a/coordinatorlayout/coordinatorlayout/api/api_lint.ignore
+++ b/coordinatorlayout/coordinatorlayout/api/api_lint.ignore
@@ -1,6 +1,4 @@
 // Baseline format: 1.0
-InvalidNullabilityOverride: androidx.coordinatorlayout.widget.CoordinatorLayout#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `c` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.coordinatorlayout.widget.CoordinatorLayout#onNestedPreScroll(android.view.View, int, int, int[]) parameter #0:
     Invalid nullability on parameter `target` in method `onNestedPreScroll`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.coordinatorlayout.widget.CoordinatorLayout#onNestedPreScroll(android.view.View, int, int, int[]) parameter #3:
@@ -35,6 +33,8 @@
     Missing nullability on method `generateLayoutParams` return
 MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#generateLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
     Missing nullability on parameter `p` in method `generateLayoutParams`
+MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `c` in method `onDraw`
 MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
     Missing nullability on parameter `ev` in method `onInterceptTouchEvent`
 MissingNullability: androidx.coordinatorlayout.widget.CoordinatorLayout#onNestedFling(android.view.View, float, float, boolean) parameter #0:
diff --git a/coordinatorlayout/coordinatorlayout/lint-baseline.xml b/coordinatorlayout/coordinatorlayout/lint-baseline.xml
index eb4748e..b3144af 100644
--- a/coordinatorlayout/coordinatorlayout/lint-baseline.xml
+++ b/coordinatorlayout/coordinatorlayout/lint-baseline.xml
@@ -1,5 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.4.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.0-alpha08)" variant="all" version="7.4.0-alpha08">
+<issues format="6" by="lint 8.0.0-alpha07" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-alpha07)" variant="all" version="8.0.0-alpha07">
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {"
+        errorLine2="                                ~~~~~~">
+        <location
+            file="src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java"/>
+    </issue>
 
     <issue
         id="UnknownNullness"
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index aa9867b..71f97fd 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1804,6 +1804,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -2148,6 +2149,66 @@
     method public static boolean addLinks(android.text.Spannable, java.util.regex.Pattern, String?, String![]?, android.text.util.Linkify.MatchFilter?, android.text.util.Linkify.TransformFilter?);
   }
 
+  public final class LocalePreferences {
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(java.util.Locale, boolean);
+  }
+
+  public static class LocalePreferences.CalendarType {
+    field public static final String CHINESE = "chinese";
+    field public static final String DANGI = "dangi";
+    field public static final String DEFAULT = "";
+    field public static final String GREGORIAN = "gregorian";
+    field public static final String HEBREW = "hebrew";
+    field public static final String INDIAN = "indian";
+    field public static final String ISLAMIC = "islamic";
+    field public static final String ISLAMIC_CIVIL = "islamic-civil";
+    field public static final String ISLAMIC_RGSA = "islamic-rgsa";
+    field public static final String ISLAMIC_TBLA = "islamic-tbla";
+    field public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+    field public static final String PERSIAN = "persian";
+  }
+
+  public static class LocalePreferences.FirstDayOfWeek {
+    field public static final String DEFAULT = "";
+    field public static final String FRIDAY = "fri";
+    field public static final String MONDAY = "mon";
+    field public static final String SATURDAY = "sat";
+    field public static final String SUNDAY = "sun";
+    field public static final String THURSDAY = "thu";
+    field public static final String TUESDAY = "tue";
+    field public static final String WEDNESDAY = "wed";
+  }
+
+  public static class LocalePreferences.HourCycle {
+    field public static final String DEFAULT = "";
+    field public static final String H11 = "h11";
+    field public static final String H12 = "h12";
+    field public static final String H23 = "h23";
+    field public static final String H24 = "h24";
+  }
+
+  public static class LocalePreferences.TemperatureUnit {
+    field public static final String CELSIUS = "celsius";
+    field public static final String DEFAULT = "";
+    field public static final String FAHRENHEIT = "fahrenheit";
+    field public static final String KELVIN = "kelvin";
+  }
+
 }
 
 package androidx.core.util {
@@ -2686,9 +2747,12 @@
     method public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
   }
 
-  @Deprecated public final class VelocityTrackerCompat {
+  public final class VelocityTrackerCompat {
+    method public static float getAxisVelocity(android.view.VelocityTracker, int);
+    method public static float getAxisVelocity(android.view.VelocityTracker, int, int);
     method @Deprecated public static float getXVelocity(android.view.VelocityTracker!, int);
     method @Deprecated public static float getYVelocity(android.view.VelocityTracker!, int);
+    method public static boolean isAxisSupported(android.view.VelocityTracker, int);
   }
 
   public class ViewCompat {
@@ -3624,6 +3688,7 @@
   }
 
   public class AccessibilityWindowInfoCompat {
+    ctor public AccessibilityWindowInfoCompat();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat? getAnchor();
     method public void getBoundsInScreen(android.graphics.Rect);
     method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat? getChild(int);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 7960fa9..0b2a58d 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1807,6 +1807,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=32, codename="Sv2") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastSv2();
     method @ChecksSdkIntAtLeast(api=33, codename="Tiramisu") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastT();
     method @ChecksSdkIntAtLeast(codename="UpsideDownCake") @androidx.core.os.BuildCompat.PrereleaseSdkCheck public static boolean isAtLeastU();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -2154,6 +2155,66 @@
     method public static boolean addLinks(android.text.Spannable, java.util.regex.Pattern, String?, String![]?, android.text.util.Linkify.MatchFilter?, android.text.util.Linkify.TransformFilter?);
   }
 
+  public final class LocalePreferences {
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(java.util.Locale, boolean);
+  }
+
+  public static class LocalePreferences.CalendarType {
+    field public static final String CHINESE = "chinese";
+    field public static final String DANGI = "dangi";
+    field public static final String DEFAULT = "";
+    field public static final String GREGORIAN = "gregorian";
+    field public static final String HEBREW = "hebrew";
+    field public static final String INDIAN = "indian";
+    field public static final String ISLAMIC = "islamic";
+    field public static final String ISLAMIC_CIVIL = "islamic-civil";
+    field public static final String ISLAMIC_RGSA = "islamic-rgsa";
+    field public static final String ISLAMIC_TBLA = "islamic-tbla";
+    field public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+    field public static final String PERSIAN = "persian";
+  }
+
+  public static class LocalePreferences.FirstDayOfWeek {
+    field public static final String DEFAULT = "";
+    field public static final String FRIDAY = "fri";
+    field public static final String MONDAY = "mon";
+    field public static final String SATURDAY = "sat";
+    field public static final String SUNDAY = "sun";
+    field public static final String THURSDAY = "thu";
+    field public static final String TUESDAY = "tue";
+    field public static final String WEDNESDAY = "wed";
+  }
+
+  public static class LocalePreferences.HourCycle {
+    field public static final String DEFAULT = "";
+    field public static final String H11 = "h11";
+    field public static final String H12 = "h12";
+    field public static final String H23 = "h23";
+    field public static final String H24 = "h24";
+  }
+
+  public static class LocalePreferences.TemperatureUnit {
+    field public static final String CELSIUS = "celsius";
+    field public static final String DEFAULT = "";
+    field public static final String FAHRENHEIT = "fahrenheit";
+    field public static final String KELVIN = "kelvin";
+  }
+
 }
 
 package androidx.core.util {
@@ -2692,9 +2753,12 @@
     method public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
   }
 
-  @Deprecated public final class VelocityTrackerCompat {
+  public final class VelocityTrackerCompat {
+    method public static float getAxisVelocity(android.view.VelocityTracker, int);
+    method public static float getAxisVelocity(android.view.VelocityTracker, int, int);
     method @Deprecated public static float getXVelocity(android.view.VelocityTracker!, int);
     method @Deprecated public static float getYVelocity(android.view.VelocityTracker!, int);
+    method public static boolean isAxisSupported(android.view.VelocityTracker, int);
   }
 
   public class ViewCompat {
@@ -3630,6 +3694,7 @@
   }
 
   public class AccessibilityWindowInfoCompat {
+    ctor public AccessibilityWindowInfoCompat();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat? getAnchor();
     method public void getBoundsInScreen(android.graphics.Rect);
     method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat? getChild(int);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 63e99d4..63cf4c9 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2144,6 +2144,7 @@
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
     method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
     method @Deprecated @ChecksSdkIntAtLeast(api=31, codename="S") public static boolean isAtLeastS();
+    field @ChecksSdkIntAtLeast(extension=android.os.ext.SdkExtensions.AD_SERVICES) public static final int AD_SERVICES_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.R) public static final int R_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.S) public static final int S_EXTENSION_INT;
     field @ChecksSdkIntAtLeast(extension=android.os.Build.VERSION_CODES.TIRAMISU) public static final int T_EXTENSION_INT;
@@ -2517,6 +2518,66 @@
   @IntDef(flag=true, value={android.text.util.Linkify.WEB_URLS, android.text.util.Linkify.EMAIL_ADDRESSES, android.text.util.Linkify.PHONE_NUMBERS, android.text.util.Linkify.MAP_ADDRESSES, android.text.util.Linkify.ALL}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface LinkifyCompat.LinkifyMask {
   }
 
+  public final class LocalePreferences {
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getCalendarType(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getFirstDayOfWeek(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getHourCycle(java.util.Locale, boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit();
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(java.util.Locale);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(boolean);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static String getTemperatureUnit(java.util.Locale, boolean);
+  }
+
+  public static class LocalePreferences.CalendarType {
+    field public static final String CHINESE = "chinese";
+    field public static final String DANGI = "dangi";
+    field public static final String DEFAULT = "";
+    field public static final String GREGORIAN = "gregorian";
+    field public static final String HEBREW = "hebrew";
+    field public static final String INDIAN = "indian";
+    field public static final String ISLAMIC = "islamic";
+    field public static final String ISLAMIC_CIVIL = "islamic-civil";
+    field public static final String ISLAMIC_RGSA = "islamic-rgsa";
+    field public static final String ISLAMIC_TBLA = "islamic-tbla";
+    field public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+    field public static final String PERSIAN = "persian";
+  }
+
+  public static class LocalePreferences.FirstDayOfWeek {
+    field public static final String DEFAULT = "";
+    field public static final String FRIDAY = "fri";
+    field public static final String MONDAY = "mon";
+    field public static final String SATURDAY = "sat";
+    field public static final String SUNDAY = "sun";
+    field public static final String THURSDAY = "thu";
+    field public static final String TUESDAY = "tue";
+    field public static final String WEDNESDAY = "wed";
+  }
+
+  public static class LocalePreferences.HourCycle {
+    field public static final String DEFAULT = "";
+    field public static final String H11 = "h11";
+    field public static final String H12 = "h12";
+    field public static final String H23 = "h23";
+    field public static final String H24 = "h24";
+  }
+
+  public static class LocalePreferences.TemperatureUnit {
+    field public static final String CELSIUS = "celsius";
+    field public static final String DEFAULT = "";
+    field public static final String FAHRENHEIT = "fahrenheit";
+    field public static final String KELVIN = "kelvin";
+  }
+
 }
 
 package androidx.core.util {
@@ -3122,9 +3183,15 @@
     method public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
   }
 
-  @Deprecated public final class VelocityTrackerCompat {
+  public final class VelocityTrackerCompat {
+    method public static float getAxisVelocity(android.view.VelocityTracker, @androidx.core.view.VelocityTrackerCompat.VelocityTrackableMotionEventAxis int);
+    method public static float getAxisVelocity(android.view.VelocityTracker, @androidx.core.view.VelocityTrackerCompat.VelocityTrackableMotionEventAxis int, int);
     method @Deprecated public static float getXVelocity(android.view.VelocityTracker!, int);
     method @Deprecated public static float getYVelocity(android.view.VelocityTracker!, int);
+    method public static boolean isAxisSupported(android.view.VelocityTracker, @androidx.core.view.VelocityTrackerCompat.VelocityTrackableMotionEventAxis int);
+  }
+
+  @IntDef({android.view.MotionEvent.AXIS_X, android.view.MotionEvent.AXIS_Y, android.view.MotionEvent.AXIS_SCROLL}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface VelocityTrackerCompat.VelocityTrackableMotionEventAxis {
   }
 
   public class ViewCompat {
@@ -4097,6 +4164,7 @@
   }
 
   public class AccessibilityWindowInfoCompat {
+    ctor public AccessibilityWindowInfoCompat();
     method public androidx.core.view.accessibility.AccessibilityNodeInfoCompat? getAnchor();
     method public void getBoundsInScreen(android.graphics.Rect);
     method public androidx.core.view.accessibility.AccessibilityWindowInfoCompat? getChild(int);
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index 2949272..ca1c7c0 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -713,6 +713,15 @@
     </issue>
 
     <issue
+        id="Range"
+        message="Value must be ≥ 1 and ≤ 200 but `getSvid` can be 206"
+        errorLine1="        return mWrapped.getSvid(satelliteIndex);"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/core/location/GnssStatusWrapper.java"/>
+    </issue>
+
+    <issue
         id="WrongConstant"
         message="Must be one of: Callback.DISPATCH_MODE_STOP, Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE"
         errorLine1="                super(compat.getDispatchMode());"
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index 85db291..9629356 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -86,6 +86,7 @@
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
 import android.app.SearchManager;
+import android.app.UiAutomation;
 import android.app.UiModeManager;
 import android.app.WallpaperManager;
 import android.app.admin.DevicePolicyManager;
@@ -517,11 +518,15 @@
     @Test
     @SdkSuppress(minSdkVersion = 29, maxSdkVersion = 32)
     public void testRegisterReceiverPermissionNotGrantedApi26() {
-        InstrumentationRegistry
-                .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-        assertThrows(RuntimeException.class,
-                () -> ContextCompat.registerReceiver(mContext,
-                        mTestReceiver, mTestFilter, ContextCompat.RECEIVER_NOT_EXPORTED));
+        UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
+        try {
+            assertThrows(RuntimeException.class,
+                    () -> ContextCompat.registerReceiver(mContext,
+                            mTestReceiver, mTestFilter, ContextCompat.RECEIVER_NOT_EXPORTED));
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
     }
 
     @Test
diff --git a/core/core/src/androidTest/java/androidx/core/text/util/LocalePreferencesTest.java b/core/core/src/androidTest/java/androidx/core/text/util/LocalePreferencesTest.java
new file mode 100644
index 0000000..c59ae1f
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/text/util/LocalePreferencesTest.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text.util;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build.VERSION_CODES;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+@SmallTest
+@SdkSuppress(minSdkVersion = VERSION_CODES.TIRAMISU)
+@RunWith(AndroidJUnit4.class)
+public class LocalePreferencesTest {
+    private static Locale sLocale;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        sLocale = Locale.getDefault(Locale.Category.FORMAT);
+    }
+
+    @After
+    public void tearDown() {
+        Locale.setDefault(sLocale);
+    }
+
+    // Hour cycle
+    @Test
+    public void getHourCycle_hasSubTags_resultIsH24() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getHourCycle();
+
+        assertEquals(LocalePreferences.HourCycle.H24, result);
+    }
+
+    @Test
+    public void getHourCycle_hasSubTagsWithoutHourCycleTag_resultIsH12() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getHourCycle();
+
+        assertEquals(LocalePreferences.HourCycle.H12, result);
+    }
+
+    @Test
+    public void getHourCycle_hasSubTagsAndDisableResolved_resultIsH24() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getHourCycle(false);
+
+        assertEquals(LocalePreferences.HourCycle.H24, result);
+    }
+
+    @Test
+    public void getHourCycle_hasSubTagsWithoutHourCycleTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getHourCycle(false);
+
+        assertEquals(LocalePreferences.HourCycle.DEFAULT, result);
+    }
+
+    @Test
+    public void getHourCycle_inputLocaleWithHourCycleTag_resultIsH12() throws Exception {
+        String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("en-US-u-hc-h12"));
+
+        assertEquals(LocalePreferences.HourCycle.H12, result);
+    }
+
+    @Test
+    public void getHourCycle_inputLocaleWithoutHourCycleTag_resultIsH12() throws Exception {
+        String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("en-US"));
+
+        assertEquals(LocalePreferences.HourCycle.H12, result);
+    }
+
+    @Test
+    public void getHourCycle_inputH23Locale_resultIsH23() throws Exception {
+        String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("fr-FR"));
+
+        assertEquals(LocalePreferences.HourCycle.H23, result);
+    }
+
+    @Test
+    public void getHourCycle_inputH23LocaleWithHourCycleTag_resultIsH12() throws Exception {
+        String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("fr-FR-u-hc-h12"));
+
+        assertEquals(LocalePreferences.HourCycle.H12, result);
+    }
+
+    @Test
+    public void getHourCycle_inputLocaleWithoutHourCycleTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        String result = LocalePreferences.getHourCycle(Locale.forLanguageTag("en-US"), false);
+
+        assertEquals(LocalePreferences.HourCycle.DEFAULT, result);
+    }
+
+    @Test
+    public void getHourCycle_compareHasResolvedValueIsTrueAndWithoutResolvedValue_sameResult()
+            throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("zh-TW-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        // Has Hour Cycle subtag
+        String resultWithoutResolvedValue = LocalePreferences.getHourCycle();
+        String resultResolvedIsTrue = LocalePreferences.getHourCycle(true);
+        assertEquals(resultWithoutResolvedValue, resultResolvedIsTrue);
+
+        // Does not have HourCycle subtag
+        Locale.setDefault(Locale.forLanguageTag("zh-TW-u-ca-chinese-mu-celsius-fw-wed"));
+
+        resultWithoutResolvedValue = LocalePreferences.getHourCycle();
+        resultResolvedIsTrue = LocalePreferences.getHourCycle(true);
+        assertEquals(resultWithoutResolvedValue, resultResolvedIsTrue);
+    }
+
+    // Calendar
+    @Test
+    public void getCalendarType_hasSubTags_resultIsChinese() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getCalendarType();
+
+        assertEquals(LocalePreferences.CalendarType.CHINESE, result);
+    }
+
+    @Test
+    public void getCalendarType_hasSubTagsWithoutCalendarTag_resultIsGregorian() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getCalendarType();
+
+        assertEquals(LocalePreferences.CalendarType.GREGORIAN, result);
+    }
+
+    @Test
+    public void getCalendarType_hasSubTagsAndDisableResolved_resultIsChinese() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getCalendarType(false);
+
+        assertEquals(LocalePreferences.CalendarType.CHINESE, result);
+    }
+
+    @Test
+    public void getCalendarType_hasSubTagsWithoutCalendarTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getCalendarType(false);
+
+        assertEquals(LocalePreferences.CalendarType.DEFAULT, result);
+    }
+
+    @Test
+    public void getCalendarType_inputLocaleWithCalendarTag_resultIsChinese() throws Exception {
+        String result =
+                LocalePreferences.getCalendarType(Locale.forLanguageTag("en-US-u-ca-chinese"));
+
+        assertEquals(LocalePreferences.CalendarType.CHINESE, result);
+    }
+
+    @Test
+    public void getCalendarType_inputLocaleWithoutCalendarTag_resultIsGregorian() throws Exception {
+        String result = LocalePreferences.getCalendarType(Locale.forLanguageTag("en-US"));
+
+        assertEquals(LocalePreferences.CalendarType.GREGORIAN, result);
+    }
+
+    @Test
+    public void getCalendarType_inputLocaleWithoutCalendarTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        String result = LocalePreferences.getCalendarType(Locale.forLanguageTag("en-US"), false);
+
+        assertEquals(LocalePreferences.CalendarType.DEFAULT, result);
+    }
+
+    // Temperature unit
+    @Test
+    public void getTemperatureUnit_hasSubTags_resultIsCelsius() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getTemperatureUnit();
+
+        assertEquals(LocalePreferences.TemperatureUnit.CELSIUS, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_hasSubTagsWithoutUnitTag_resultIsFahrenheit() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-hc-h24-fw-wed"));
+
+        String result = LocalePreferences.getTemperatureUnit();
+
+        assertEquals(LocalePreferences.TemperatureUnit.FAHRENHEIT, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_hasSubTagsAndDisableResolved_resultIsCelsius() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getTemperatureUnit(false);
+
+        assertEquals(LocalePreferences.TemperatureUnit.CELSIUS, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_hasSubTagsAndDisableResolved_resultIsFahrenheit()
+            throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("zh-TW-u-ca-chinese-hc-h24-mu-fahrenhe-fw-wed"));
+
+        String result = LocalePreferences.getTemperatureUnit(false);
+
+        assertEquals(LocalePreferences.TemperatureUnit.FAHRENHEIT, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_hasSubTagsWithoutUnitTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-fw-wed"));
+
+        String result = LocalePreferences.getTemperatureUnit(false);
+
+        assertEquals(LocalePreferences.TemperatureUnit.DEFAULT, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_inputLocaleWithUnitTag_resultIsCelsius() throws Exception {
+        String result = LocalePreferences
+                .getTemperatureUnit(Locale.forLanguageTag("en-US-u-mu-celsius"));
+
+        assertEquals(LocalePreferences.TemperatureUnit.CELSIUS, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_inputLocaleWithoutUnitTag_resultIsFahrenheit() throws Exception {
+        String result = LocalePreferences.getTemperatureUnit(Locale.forLanguageTag("en-US"));
+
+        assertEquals(LocalePreferences.TemperatureUnit.FAHRENHEIT, result);
+    }
+
+    @Test
+    public void getTemperatureUnit_inputLocaleWithoutUnitTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        String result = LocalePreferences
+                .getTemperatureUnit(Locale.forLanguageTag("en-US"), false);
+
+        assertEquals(LocalePreferences.TemperatureUnit.DEFAULT, result);
+    }
+
+    // First day of week
+    @Test
+    public void getFirstDayOfWeek_hasSubTags_resultIsCelsius() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getFirstDayOfWeek();
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.WEDNESDAY, result);
+    }
+
+    @Test
+    public void getFirstDayOfWeek_hasSubTagsWithoutFwTag_resultIsSun() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-hc-h24"));
+
+        String result = LocalePreferences.getFirstDayOfWeek();
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.SUNDAY, result);
+
+    }
+
+    @Test
+    public void getFirstDayOfWeek_hasSubTagsAndDisableResolved_resultIsWed() throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese-hc-h24-mu-celsius-fw-wed"));
+
+        String result = LocalePreferences.getFirstDayOfWeek(false);
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.WEDNESDAY, result);
+    }
+
+    @Test
+    public void getFirstDayOfWeek_hasSubTagsWithoutFwTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        Locale.setDefault(Locale.forLanguageTag("en-US-u-ca-chinese"));
+
+        String result = LocalePreferences.getFirstDayOfWeek(false);
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.DEFAULT, result);
+    }
+
+    @Test
+    public void getFirstDayOfWeek_inputLocaleWithFwTag_resultIsWed() throws Exception {
+        String result = LocalePreferences
+                .getFirstDayOfWeek(Locale.forLanguageTag("en-US-u-fw-wed"));
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.WEDNESDAY, result);
+    }
+
+    @Test
+    public void getFirstDayOfWeek_inputLocaleWithoutFwTag_resultIsSun() throws Exception {
+        String result = LocalePreferences.getFirstDayOfWeek(Locale.forLanguageTag("en-US"));
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.SUNDAY, result);
+    }
+
+    @Test
+    public void getFirstDayOfWeek_inputLocaleWithoutFwTagAndDisableResolved_resultIsEmpty()
+            throws Exception {
+        String result = LocalePreferences
+                .getFirstDayOfWeek(Locale.forLanguageTag("en-US"), false);
+
+        assertEquals(LocalePreferences.FirstDayOfWeek.DEFAULT, result);
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/VelocityTrackerCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/VelocityTrackerCompatTest.java
new file mode 100644
index 0000000..b35399a
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/VelocityTrackerCompatTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.core.view;
+
+import static android.view.MotionEvent.AXIS_BRAKE;
+import static android.view.MotionEvent.AXIS_X;
+import static android.view.MotionEvent.AXIS_Y;
+
+import static androidx.core.view.MotionEventCompat.AXIS_SCROLL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import android.os.Build;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VelocityTrackerCompatTest {
+    @Mock private VelocityTracker mTracker;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testIsAxisSupported_planarAxes() {
+        assertTrue(VelocityTrackerCompat.isAxisSupported(mTracker, AXIS_X));
+        assertTrue(VelocityTrackerCompat.isAxisSupported(mTracker, AXIS_Y));
+    }
+
+    @Test
+    public void testIsAxisSupported_nonPlanarAxes() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            when(mTracker.isAxisSupported(MotionEvent.AXIS_SCROLL)).thenReturn(true);
+
+            assertTrue(VelocityTrackerCompat.isAxisSupported(mTracker, AXIS_SCROLL));
+        } else {
+            assertFalse(
+                    VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_SCROLL));
+        }
+
+        // Check against an axis that has not yet been supported at any Android version.
+        assertFalse(VelocityTrackerCompat.isAxisSupported(VelocityTracker.obtain(), AXIS_BRAKE));
+    }
+
+    @Test
+    public void testGetAxisVelocity() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            when(mTracker.getAxisVelocity(AXIS_X)).thenReturn(1f);
+            when(mTracker.getAxisVelocity(AXIS_Y)).thenReturn(2f);
+            when(mTracker.getAxisVelocity(AXIS_SCROLL)).thenReturn(3f);
+
+            assertEquals(1f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_X), 0);
+            assertEquals(2f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_Y), 0);
+            assertEquals(3f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_SCROLL), 0);
+        } else {
+            when(mTracker.getXVelocity()).thenReturn(2f);
+            when(mTracker.getYVelocity()).thenReturn(3f);
+
+            assertEquals(2f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_X), 0);
+            assertEquals(3f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_Y), 0);
+            // AXIS_SCROLL not supported before API 34.
+            assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_SCROLL), 0);
+        }
+
+        // Check against an axis that has not yet been supported at any Android version.
+        assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_BRAKE), 0);
+    }
+
+    @Test
+    public void testGetAxisVelocity_withPointerId() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            when(mTracker.getAxisVelocity(AXIS_X, 4)).thenReturn(1f);
+            when(mTracker.getAxisVelocity(AXIS_Y, 5)).thenReturn(2f);
+            when(mTracker.getAxisVelocity(AXIS_SCROLL, 1)).thenReturn(3f);
+
+            assertEquals(4f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_X, 4), 0);
+            assertEquals(5f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_Y, 5), 0);
+            assertEquals(3f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_SCROLL, 1), 0);
+            // Test with pointer IDs with no velocity.
+            assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_X, 2), 0);
+            assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_Y, 2), 0);
+            assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_SCROLL, 2), 0);
+        } else {
+            when(mTracker.getXVelocity(2)).thenReturn(2f);
+            when(mTracker.getYVelocity(3)).thenReturn(3f);
+
+            // Test with pointer IDs with no velocity.
+            assertEquals(2f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_X, 2), 0);
+            assertEquals(3f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_Y, 3), 0);
+            // AXIS_SCROLL not supported before API 34.
+            assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_SCROLL, 2), 0);
+        }
+
+        // Check against an axis that has not yet been supported at any Android version.
+        assertEquals(0f, VelocityTrackerCompat.getAxisVelocity(mTracker, AXIS_BRAKE, 4), 0);
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java
index a1afdfda..1788e22 100644
--- a/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompatTest.java
@@ -17,7 +17,9 @@
 package androidx.core.view.accessibility;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNot.not;
 
 import android.annotation.TargetApi;
 import android.graphics.Region;
@@ -40,6 +42,17 @@
         return AccessibilityWindowInfoCompat.wrapNonNullInstance(accessibilityWindowInfo);
     }
 
+    @SdkSuppress(minSdkVersion = 30)
+    @SmallTest
+    @Test
+    public void testConstructor() {
+        AccessibilityWindowInfoCompat infoCompat = new AccessibilityWindowInfoCompat();
+        AccessibilityWindowInfo info = new AccessibilityWindowInfo();
+
+        assertThat(infoCompat.unwrap(), is(not(equalTo(null))));
+        assertThat(infoCompat.unwrap(), equalTo(info));
+    }
+
     @SdkSuppress(minSdkVersion = 33)
     @SmallTest
     @Test
diff --git a/core/core/src/main/java/androidx/core/location/LocationCompat.java b/core/core/src/main/java/androidx/core/location/LocationCompat.java
index 6ccefa8..2e2c045 100644
--- a/core/core/src/main/java/androidx/core/location/LocationCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationCompat.java
@@ -81,7 +81,8 @@
     @Nullable
     private static Method sSetIsFromMockProviderMethod;
 
-    private LocationCompat() {}
+    private LocationCompat() {
+    }
 
     /**
      * Return the time of this fix, in nanoseconds of elapsed real-time since system boot.
@@ -295,9 +296,17 @@
     /**
      * Returns the Mean Sea Level altitude of the location in meters.
      *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+     * order to allow for backwards compatibility and testing however, this method will attempt
+     * to read a double extra with the key {@link #EXTRA_MSL_ALTITUDE} and return the result.
+     *
      * @throws IllegalStateException if the Mean Sea Level altitude of the location is not set
+     * @see Location#getMslAltitudeMeters()
      */
     public static double getMslAltitudeMeters(@NonNull Location location) {
+        if (VERSION.SDK_INT >= 34) {
+            return Api34Impl.getMslAltitudeMeters(location);
+        }
         Preconditions.checkState(hasMslAltitude(location),
                 "The Mean Sea Level altitude of the location is not set.");
         return getOrCreateExtras(location).getDouble(EXTRA_MSL_ALTITUDE);
@@ -305,24 +314,54 @@
 
     /**
      * Sets the Mean Sea Level altitude of the location in meters.
+     *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+     * order to allow for backwards compatibility and testing however, this method will attempt
+     * to set a double extra with the key {@link #EXTRA_MSL_ALTITUDE} to include Mean Sea Level
+     * altitude. Be aware that this will overwrite any prior extra value under the same key.
+     *
+     * @see Location#setMslAltitudeMeters(double)
      */
     public static void setMslAltitudeMeters(@NonNull Location location,
             double mslAltitudeMeters) {
-        getOrCreateExtras(location).putDouble(EXTRA_MSL_ALTITUDE, mslAltitudeMeters);
+        if (VERSION.SDK_INT >= 34) {
+            Api34Impl.setMslAltitudeMeters(location, mslAltitudeMeters);
+        } else {
+            getOrCreateExtras(location).putDouble(EXTRA_MSL_ALTITUDE, mslAltitudeMeters);
+        }
     }
 
     /**
      * Returns true if the location has a Mean Sea Level altitude, false otherwise.
+     *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+     * order to allow for backwards compatibility and testing however, this method will return
+     * true if an extra value is with the key {@link #EXTRA_MSL_ALTITUDE}.
+     *
+     * @see Location#hasMslAltitude()
      */
     public static boolean hasMslAltitude(@NonNull Location location) {
+        if (VERSION.SDK_INT >= 34) {
+            return Api34Impl.hasMslAltitude(location);
+        }
         return containsExtra(location, EXTRA_MSL_ALTITUDE);
     }
 
     /**
      * Removes the Mean Sea Level altitude from the location.
+     *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude does not exist. In
+     * order to allow for backwards compatibility and testing however, this method will attempt
+     * to remove any extra value with the key {@link #EXTRA_MSL_ALTITUDE}.
+     *
+     * @see Location#removeMslAltitude()
      */
     public static void removeMslAltitude(@NonNull Location location) {
-        removeExtra(location, EXTRA_MSL_ALTITUDE);
+        if (VERSION.SDK_INT >= 34) {
+            Api34Impl.removeMslAltitude(location);
+        } else {
+            removeExtra(location, EXTRA_MSL_ALTITUDE);
+        }
     }
 
     /**
@@ -331,11 +370,20 @@
      * altitude of the location falls within {@link #getMslAltitudeMeters(Location)} +/- this
      * uncertainty.
      *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+     * exist. In order to allow for backwards compatibility and testing however, this method will
+     * attempt to read a float extra with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY} and return
+     * the result.
+     *
      * @throws IllegalStateException if the Mean Sea Level altitude accuracy of the location is not
      *                               set
+     * @see Location#setMslAltitudeAccuracyMeters(float)
      */
     public static @FloatRange(from = 0.0) float getMslAltitudeAccuracyMeters(
             @NonNull Location location) {
+        if (VERSION.SDK_INT >= 34) {
+            return Api34Impl.getMslAltitudeAccuracyMeters(location);
+        }
         Preconditions.checkState(hasMslAltitudeAccuracy(location),
                 "The Mean Sea Level altitude accuracy of the location is not set.");
         return getOrCreateExtras(location).getFloat(EXTRA_MSL_ALTITUDE_ACCURACY);
@@ -343,25 +391,56 @@
 
     /**
      * Sets the Mean Sea Level altitude accuracy of the location in meters.
+     *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+     * exist. In order to allow for backwards compatibility and testing however, this method will
+     * attempt to set a float extra with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY} to include
+     * Mean Sea Level altitude accuracy. Be aware that this will overwrite any prior extra value
+     * under the same key.
+     *
+     * @see Location#setMslAltitudeAccuracyMeters(float)
      */
     public static void setMslAltitudeAccuracyMeters(@NonNull Location location,
             @FloatRange(from = 0.0) float mslAltitudeAccuracyMeters) {
-        getOrCreateExtras(location).putFloat(EXTRA_MSL_ALTITUDE_ACCURACY,
-                mslAltitudeAccuracyMeters);
+        if (VERSION.SDK_INT >= 34) {
+            Api34Impl.setMslAltitudeAccuracyMeters(location, mslAltitudeAccuracyMeters);
+        } else {
+            getOrCreateExtras(location).putFloat(EXTRA_MSL_ALTITUDE_ACCURACY,
+                    mslAltitudeAccuracyMeters);
+        }
     }
 
     /**
      * Returns true if the location has a Mean Sea Level altitude accuracy, false otherwise.
+     *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+     * exist. In order to allow for backwards compatibility and testing however, this method will
+     * return true if an extra value is with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY}.
+     *
+     * @see Location#hasMslAltitudeAccuracy()
      */
     public static boolean hasMslAltitudeAccuracy(@NonNull Location location) {
+        if (VERSION.SDK_INT >= 34) {
+            return Api34Impl.hasMslAltitudeAccuracy(location);
+        }
         return containsExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
     }
 
     /**
      * Removes the Mean Sea Level altitude accuracy from the location.
+     *
+     * <p>NOTE: On API levels below 34, the concept of Mean Sea Level altitude accuracy does not
+     * exist. In order to allow for backwards compatibility and testing however, this method will
+     * attempt to remove any extra value with the key {@link #EXTRA_MSL_ALTITUDE_ACCURACY}.
+     *
+     * @see Location#removeMslAltitudeAccuracy()
      */
     public static void removeMslAltitudeAccuracy(@NonNull Location location) {
-        removeExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
+        if (VERSION.SDK_INT >= 34) {
+            Api34Impl.removeMslAltitudeAccuracy(location);
+        } else {
+            removeExtra(location, EXTRA_MSL_ALTITUDE_ACCURACY);
+        }
     }
 
     /**
@@ -433,10 +512,59 @@
         }
     }
 
+    @RequiresApi(34)
+    private static class Api34Impl {
+
+        private Api34Impl() {
+        }
+
+        @DoNotInline
+        static double getMslAltitudeMeters(Location location) {
+            return location.getMslAltitudeMeters();
+        }
+
+        @DoNotInline
+        static void setMslAltitudeMeters(Location location, double mslAltitudeMeters) {
+            location.setMslAltitudeMeters(mslAltitudeMeters);
+        }
+
+        @DoNotInline
+        static boolean hasMslAltitude(Location location) {
+            return location.hasMslAltitude();
+        }
+
+        @DoNotInline
+        static void removeMslAltitude(Location location) {
+            location.removeMslAltitude();
+        }
+
+        @DoNotInline
+        static float getMslAltitudeAccuracyMeters(Location location) {
+            return location.getMslAltitudeAccuracyMeters();
+        }
+
+        @DoNotInline
+        static void setMslAltitudeAccuracyMeters(Location location,
+                float mslAltitudeAccuracyMeters) {
+            location.setMslAltitudeAccuracyMeters(mslAltitudeAccuracyMeters);
+        }
+
+        @DoNotInline
+        static boolean hasMslAltitudeAccuracy(Location location) {
+            return location.hasMslAltitudeAccuracy();
+        }
+
+        @DoNotInline
+        static void removeMslAltitudeAccuracy(Location location) {
+            location.removeMslAltitudeAccuracy();
+        }
+    }
+
     @RequiresApi(26)
     private static class Api26Impl {
 
-        private Api26Impl() {}
+        private Api26Impl() {
+        }
 
         @DoNotInline
         static boolean hasVerticalAccuracy(Location location) {
@@ -487,7 +615,8 @@
     @RequiresApi(18)
     private static class Api18Impl {
 
-        private Api18Impl() {}
+        private Api18Impl() {
+        }
 
         @DoNotInline
         static boolean isMock(Location location) {
@@ -498,7 +627,8 @@
     @RequiresApi(17)
     private static class Api17Impl {
 
-        private Api17Impl() {}
+        private Api17Impl() {
+        }
 
         @DoNotInline
         static long getElapsedRealtimeNanos(Location location) {
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index e50f09e..223fba1 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint;
 import android.os.Build;
 import android.os.Build.VERSION;
+import android.os.ext.SdkExtensions;
 
 import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
@@ -281,6 +282,21 @@
     @SuppressLint("CompileTimeConstant")
     public static final int T_EXTENSION_INT = VERSION.SDK_INT >= 30 ? Extensions30Impl.TIRAMISU : 0;
 
+    /**
+     * The value of {@code SdkExtensions.getExtensionVersion(AD_SERVICES)}. This is a convenience
+     * constant which provides the extension version in a similar style to
+     * {@code Build.VERSION.SDK_INT}.
+     * <p>
+     * Compared to calling {@code getExtensionVersion} directly, using this constant has the
+     * benefit of not having to verify the {@code getExtensionVersion} method is available.
+     *
+     * @return the version of the AdServices extension, if it exists. 0 otherwise.
+     */
+    @ChecksSdkIntAtLeast(extension = SdkExtensions.AD_SERVICES)
+    @SuppressLint("CompileTimeConstant")
+    public static final int AD_SERVICES_EXTENSION_INT =
+            VERSION.SDK_INT >= 30 ? Extensions30Impl.AD_SERVICES : 0;
+
     @SuppressLint("ClassVerificationFailure") // Remove when SDK including b/206996004 is imported
     @RequiresApi(30)
     private static final class Extensions30Impl {
@@ -290,6 +306,8 @@
         static final int S = getExtensionVersion(Build.VERSION_CODES.S);
         @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
         static final int TIRAMISU = getExtensionVersion(Build.VERSION_CODES.TIRAMISU);
+        @SuppressLint("NewApi") // Remove when SDK including b/206996004 is imported
+        static final int AD_SERVICES = getExtensionVersion(SdkExtensions.AD_SERVICES);
     }
 
 }
diff --git a/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java b/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
new file mode 100644
index 0000000..95fa07b
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/text/util/LocalePreferences.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text.util;
+
+import android.icu.number.LocalizedNumberFormatter;
+import android.icu.number.NumberFormatter;
+import android.icu.text.DateFormat;
+import android.icu.text.DateTimePatternGenerator;
+import android.icu.util.MeasureUnit;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.core.os.BuildCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+import java.util.Locale.Category;
+
+/**
+ * Provides friendly APIs to get the user's locale preferences. The data can refer to
+ * external/cldr/common/main/en.xml.
+ */
+public final class LocalePreferences {
+    private static final String TAG = LocalePreferences.class.getSimpleName();
+
+    /** APIs to get the user's preference of the hour cycle. */
+    public static class HourCycle {
+        private static final String U_EXTENSION_OF_HOUR_CYCLE = "hc";
+
+        /** 12 Hour System (0-11) */
+        public static final String H11 = "h11";
+        /** 12 Hour System (1-12) */
+        public static final String H12 = "h12";
+        /** 24 Hour System (0-23) */
+        public static final String H23 = "h23";
+        /** 24 Hour System (1-24) */
+        public static final String H24 = "h24";
+        /** Default hour cycle for the locale */
+        public static final String DEFAULT = "";
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @StringDef({
+                H11,
+                H12,
+                H23,
+                H24,
+                DEFAULT
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface HourCycleTypes {
+        }
+
+        private HourCycle() {
+        }
+    }
+
+    /**
+     * Return the user's preference of the hour cycle which is from
+     * {@link Locale#getDefault(Locale.Category)}. The returned result is resolved and
+     * bases on the {@code Locale#getDefault(Locale.Category)}. E.g. "h23"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @HourCycle.HourCycleTypes
+    public static String getHourCycle() {
+        return getHourCycle(true);
+    }
+
+    /**
+     * Return the hour cycle setting of the inputted {@link Locale}. The returned result is resolved
+     * and bases on the inputted {@code Locale}.
+     * E.g. "h23"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @HourCycle.HourCycleTypes
+    public static String getHourCycle(@NonNull Locale locale) {
+        return getHourCycle(locale, true);
+    }
+
+    /**
+     * Return the user's preference of the hour cycle which is from
+     * {@link Locale#getDefault(Locale.Category)}. E.g. "h23"
+     *
+     * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains hour cycle subtag,
+     *                 this argument is ignored. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain hour cycle subtag
+     *                 and the resolved argument is true, this function tries to find the default
+     *                 hour cycle for the {@code Locale#getDefault(Locale.Category)}. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain hour cycle subtag
+     *                 and the resolved argument is false, this function returns empty string
+     *                 i.e. HourCycle.Default.
+     * @return {@link HourCycle.HourCycleTypes} If the malformed hour cycle format was specified
+     * in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string
+     * i.e. HourCycle.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @HourCycle.HourCycleTypes
+    public static String getHourCycle(
+            boolean resolved) {
+        return getHourCycle(Api33Impl.getDefaultLocale(), resolved);
+    }
+
+    /**
+     * Return the hour cycle setting of the inputted {@link Locale}. E.g. "en-US-u-hc-h23".
+     *
+     * @param locale   The {@code Locale} to get the hour cycle.
+     * @param resolved If the given {@code Locale} contains hour cycle subtag, this argument is
+     *                 ignored. If the given {@code Locale} doesn't contain hour cycle subtag and
+     *                 the resolved argument is true, this function tries to find the default
+     *                 hour cycle for the given {@code Locale}. If the given {@code Locale} doesn't
+     *                 contain hour cycle subtag and the resolved argument is false, this function
+     *                 return empty string i.e. HourCycle.Default.
+     * @return {@link HourCycle.HourCycleTypes} If the malformed hour cycle format was specified
+     * in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string
+     * i.e. HourCycle.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @HourCycle.HourCycleTypes
+    public static String getHourCycle(@NonNull Locale locale, boolean resolved) {
+        if (!BuildCompat.isAtLeastT()) {
+            throw new IllegalArgumentException("not a valid extension: " + VERSION.SDK_INT);
+        }
+        return Api33Impl.getHourCycle(locale, resolved);
+    }
+
+    /** APIs to get the user's preference of Calendar. */
+    public static class CalendarType {
+        private static final String U_EXTENSION_OF_CALENDAR = "ca";
+        /** Chinese Calendar */
+        public static final String CHINESE = "chinese";
+        /** Dangi Calendar (Korea Calendar) */
+        public static final String DANGI = "dangi";
+        /** Gregorian Calendar */
+        public static final String GREGORIAN = "gregorian";
+        /** Hebrew Calendar */
+        public static final String HEBREW = "hebrew";
+        /** Indian National Calendar */
+        public static final String INDIAN = "indian";
+        /** Islamic Calendar */
+        public static final String ISLAMIC = "islamic";
+        /** Islamic Calendar (tabular, civil epoch) */
+        public static final String ISLAMIC_CIVIL = "islamic-civil";
+        /** Islamic Calendar (Saudi Arabia, sighting) */
+        public static final String ISLAMIC_RGSA = "islamic-rgsa";
+        /** Islamic Calendar (tabular, astronomical epoch) */
+        public static final String ISLAMIC_TBLA = "islamic-tbla";
+        /** Islamic Calendar (Umm al-Qura) */
+        public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
+        /** Persian Calendar */
+        public static final String PERSIAN = "persian";
+        /** Default calendar for the locale */
+        public static final String DEFAULT = "";
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @StringDef({
+                CHINESE,
+                DANGI,
+                GREGORIAN,
+                HEBREW,
+                INDIAN,
+                ISLAMIC,
+                ISLAMIC_CIVIL,
+                ISLAMIC_RGSA,
+                ISLAMIC_TBLA,
+                ISLAMIC_UMALQURA,
+                PERSIAN,
+                DEFAULT
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface CalendarTypes {
+        }
+
+        private CalendarType() {
+        }
+    }
+
+    /**
+     * Return the user's preference of the calendar type which is from {@link
+     * Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on
+     * the {@code Locale#getDefault(Locale.Category)} settings. E.g. "chinese"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @CalendarType.CalendarTypes
+    public static String getCalendarType() {
+        return getCalendarType(true);
+    }
+
+    /**
+     * Return the calendar type of the inputted {@link Locale}. The returned result is resolved and
+     * bases on the inputted {@link Locale} settings.
+     * E.g. "chinese"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @CalendarType.CalendarTypes
+    public static String getCalendarType(@NonNull Locale locale) {
+        return getCalendarType(locale, true);
+    }
+
+    /**
+     * Return the user's preference of the calendar type which is from {@link
+     * Locale#getDefault(Locale.Category)}. E.g. "chinese"
+     *
+     * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains calendar type
+     *                 subtag, this argument is ignored. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain calendar type
+     *                 subtag and the resolved argument is true, this function tries to find
+     *                 the default calendar type for the
+     *                 {@code Locale#getDefault(Locale.Category)}. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain calendar type
+     *                 subtag and the resolved argument is false, this function returns empty string
+     *                 i.e. CalendarTypes.Default.
+     * @return {@link CalendarType.CalendarTypes} If the malformed calendar type format was
+     * specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
+     * empty string i.e. CalendarTypes.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @CalendarType.CalendarTypes
+    public static String getCalendarType(boolean resolved) {
+        return getCalendarType(Api33Impl.getDefaultLocale(), resolved);
+    }
+
+    /**
+     * Return the calendar type of the inputted {@link Locale}. E.g. "chinese"
+     *
+     * @param locale   The {@link Locale} to get the calendar type.
+     * @param resolved If the given {@code Locale} contains calendar type subtag, this argument is
+     *                 ignored. If the given {@code Locale} doesn't contain calendar type subtag and
+     *                 the resolved argument is true, this function tries to find the default
+     *                 calendar type for the given {@code Locale}. If the given {@code Locale}
+     *                 doesn't contain calendar type subtag and the resolved argument is false, this
+     *                 function return empty string i.e. CalendarTypes.Default.
+     * @return {@link CalendarType.CalendarTypes} If the malformed calendar type format was
+     * specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
+     * empty string i.e. CalendarTypes.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @CalendarType.CalendarTypes
+    public static String getCalendarType(@NonNull Locale locale, boolean resolved) {
+        if (!BuildCompat.isAtLeastT()) {
+            throw new IllegalArgumentException("not a valid extension: " + VERSION.SDK_INT);
+        }
+        return Api33Impl.getCalendarType(locale, resolved);
+    }
+
+    /** APIs to get the user's preference of temperature unit. */
+    public static class TemperatureUnit {
+        private static final String U_EXTENSION_OF_TEMPERATURE_UNIT = "mu";
+        /** Celsius */
+        public static final String CELSIUS = "celsius";
+        /** Fahrenheit */
+        public static final String FAHRENHEIT = "fahrenheit";
+        /** Kelvin */
+        public static final String KELVIN = "kelvin";
+        /** Default Temperature for the locale */
+        public static final String DEFAULT = "";
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @StringDef({
+                CELSIUS,
+                FAHRENHEIT,
+                KELVIN,
+                DEFAULT
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface TemperatureUnits {
+        }
+
+        private TemperatureUnit() {
+        }
+    }
+
+    /**
+     * Return the user's preference of the temperature unit which is from {@link
+     * Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on the
+     * {@code Locale#getDefault(Locale.Category)} settings. E.g. "fahrenheit"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @TemperatureUnit.TemperatureUnits
+    public static String getTemperatureUnit() {
+        return getTemperatureUnit(true);
+    }
+
+    /**
+     * Return the temperature unit of the inputted {@link Locale}. The returned result is resolved
+     * and bases on the inputted {@code Locale} settings. E.g. "fahrenheit"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @TemperatureUnit.TemperatureUnits
+    public static String getTemperatureUnit(
+            @NonNull Locale locale) {
+        return getTemperatureUnit(locale, true);
+    }
+
+    /**
+     * Return the user's preference of the temperature unit which is from {@link
+     * Locale#getDefault(Locale.Category)}. E.g. "fahrenheit"
+     *
+     * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains temperature unit
+     *                 subtag, this argument is ignored. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain temperature unit
+     *                 subtag and the resolved argument is true, this function tries to find
+     *                 the default temperature unit for the
+     *                 {@code Locale#getDefault(Locale.Category)}. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain temperature unit
+     *                 subtag and the resolved argument is false, this function returns empty string
+     *                 i.e. TemperatureUnits.Default.
+     * @return {@link TemperatureUnit.TemperatureUnits} If the malformed temperature unit format was
+     * specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
+     * empty string i.e. TemperatureUnits.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @TemperatureUnit.TemperatureUnits
+    public static String getTemperatureUnit(boolean resolved) {
+        return getTemperatureUnit(Api33Impl.getDefaultLocale(), resolved);
+    }
+
+    /**
+     * Return the temperature unit of the inputted {@link Locale}. E.g. "fahrenheit"
+     *
+     * @param locale   The {@link Locale} to get the temperature unit.
+     * @param resolved If the given {@code Locale} contains temperature unit subtag, this argument
+     *                 is ignored. If the given {@code Locale} doesn't contain temperature unit
+     *                 subtag and the resolved argument is true, this function tries to find
+     *                 the default temperature unit for the given {@code Locale}. If the given
+     *                 {@code Locale} doesn't contain temperature unit subtag and the resolved
+     *                 argument is false, this function return empty string
+     *                 i.e. TemperatureUnits.Default.
+     * @return {@link TemperatureUnit.TemperatureUnits} If the malformed temperature unit format was
+     * specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
+     * empty string i.e. TemperatureUnits.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @TemperatureUnit.TemperatureUnits
+    public static String getTemperatureUnit(@NonNull Locale locale, boolean resolved) {
+        if (!BuildCompat.isAtLeastT()) {
+            throw new IllegalArgumentException("not a valid extension: " + VERSION.SDK_INT);
+        }
+        return Api33Impl.getTemperatureUnit(locale, resolved);
+    }
+
+    /** APIs to get the user's preference of the first day of week. */
+    public static class FirstDayOfWeek {
+        private static final String U_EXTENSION_OF_FIRST_DAY_OF_WEEK = "fw";
+        /** Sunday */
+        public static final String SUNDAY = "sun";
+        /** Monday */
+        public static final String MONDAY = "mon";
+        /** Tuesday */
+        public static final String TUESDAY = "tue";
+        /** Wednesday */
+        public static final String WEDNESDAY = "wed";
+        /** Thursday */
+        public static final String THURSDAY = "thu";
+        /** Friday */
+        public static final String FRIDAY = "fri";
+        /** Saturday */
+        public static final String SATURDAY = "sat";
+        /** Default first day of week for the locale */
+        public static final String DEFAULT = "";
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @StringDef({
+                SUNDAY,
+                MONDAY,
+                TUESDAY,
+                WEDNESDAY,
+                THURSDAY,
+                FRIDAY,
+                SATURDAY,
+                DEFAULT
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface Days {
+        }
+
+        private FirstDayOfWeek() {
+        }
+    }
+
+    /**
+     * Return the user's preference of the first day of week which is from
+     * {@link Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on the
+     * {@code Locale#getDefault(Locale.Category)} settings. E.g. "sun"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @FirstDayOfWeek.Days
+    public static String getFirstDayOfWeek() {
+        return getFirstDayOfWeek(true);
+    }
+
+    /**
+     * Return the first day of week of the inputted {@link Locale}. The returned result is resolved
+     * and bases on the inputted {@code Locale} settings.
+     * E.g. "sun"
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    public static @FirstDayOfWeek.Days String getFirstDayOfWeek(@NonNull Locale locale) {
+        return getFirstDayOfWeek(locale, true);
+    }
+
+    /**
+     * Return the user's preference of the first day of week which is from {@link
+     * Locale#getDefault(Locale.Category)}. E.g. "sun"
+     *
+     * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains first day of week
+     *                 subtag, this argument is ignored. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain first day of week
+     *                 subtag and the resolved argument is true, this function tries to find
+     *                 the default first day of week for the
+     *                 {@code Locale#getDefault(Locale.Category)}. If the
+     *                 {@code Locale#getDefault(Locale.Category)} doesn't contain first day of week
+     *                 subtag and the resolved argument is false, this function returns empty string
+     *                 i.e. Days.Default.
+     * @return {@link FirstDayOfWeek.Days} If the malformed first day of week format was specified
+     * in the first day of week subtag, e.g. en-US-u-fw-days, this function returns empty string
+     * i.e. Days.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @FirstDayOfWeek.Days
+    public static String getFirstDayOfWeek(boolean resolved) {
+        return getFirstDayOfWeek(Api33Impl.getDefaultLocale(), resolved);
+    }
+
+    /**
+     * Return the first day of week of the inputted {@link Locale}. E.g. "sun"
+     *
+     * @param locale   The {@link Locale} to get the first day of week.
+     * @param resolved If the given {@code Locale} contains first day of week subtag, this argument
+     *                 is ignored. If the given {@code Locale} doesn't contain first day of week
+     *                 subtag and the resolved argument is true, this function tries to find
+     *                 the default first day of week for the given {@code Locale}. If the given
+     *                 {@code Locale} doesn't contain first day of week subtag and the resolved
+     *                 argument is false, this function return empty string i.e. Days.Default.
+     * @return {@link FirstDayOfWeek.Days} If the malformed first day of week format was
+     * specified in the first day of week subtag, e.g. en-US-u-fw-days, this function returns
+     * empty string i.e. Days.Default.
+     */
+    @NonNull
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    @OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
+    @FirstDayOfWeek.Days
+    public static String getFirstDayOfWeek(
+            @NonNull Locale locale, boolean resolved) {
+        if (!BuildCompat.isAtLeastT()) {
+            throw new IllegalArgumentException("not a valid extension: " + VERSION.SDK_INT);
+        }
+
+        return Api33Impl.getFirstDayOfWeek(locale, resolved);
+    }
+
+    @RequiresApi(VERSION_CODES.TIRAMISU)
+    private static class Api33Impl {
+        @DoNotInline
+        @HourCycle.HourCycleTypes
+        static String getHourCycle(@NonNull Locale locale,
+                boolean resolved) {
+            String hc = locale.getUnicodeLocaleType(HourCycle.U_EXTENSION_OF_HOUR_CYCLE);
+            if (hc != null) {
+                return hc;
+            }
+            if (!resolved) {
+                return HourCycle.DEFAULT;
+            }
+
+            return getHourCycleType(
+                    DateTimePatternGenerator.getInstance(locale).getDefaultHourCycle());
+
+        }
+
+        @DoNotInline
+        @CalendarType.CalendarTypes
+        static String getCalendarType(@NonNull Locale locale, boolean resolved) {
+            String ca = locale.getUnicodeLocaleType(CalendarType.U_EXTENSION_OF_CALENDAR);
+            if (ca != null) {
+                return ca;
+            }
+            if (!resolved) {
+                return CalendarType.DEFAULT;
+            }
+
+            return android.icu.util.Calendar.getInstance(locale).getType();
+        }
+
+        @DoNotInline
+        @TemperatureUnit.TemperatureUnits
+        static String getTemperatureUnit(@NonNull Locale locale, boolean resolved) {
+            String mu =
+                    locale.getUnicodeLocaleType(TemperatureUnit.U_EXTENSION_OF_TEMPERATURE_UNIT);
+            if (mu != null) {
+                if (mu.contains("fahrenhe")) {
+                    mu = TemperatureUnit.FAHRENHEIT;
+                }
+                return mu;
+            }
+            if (!resolved) {
+                return TemperatureUnit.DEFAULT;
+            }
+
+            return getResolvedTemperatureUnit(locale);
+        }
+
+        @DoNotInline
+        @FirstDayOfWeek.Days
+        static String getFirstDayOfWeek(@NonNull Locale locale, boolean resolved) {
+            String mu =
+                    locale.getUnicodeLocaleType(FirstDayOfWeek.U_EXTENSION_OF_FIRST_DAY_OF_WEEK);
+            if (mu != null) {
+                return mu;
+            }
+            if (!resolved) {
+                return FirstDayOfWeek.DEFAULT;
+            }
+            // TODO(b/262294472) Use {@code android.icu.util.Calendar} instead of
+            //  {@code java.util.Calendar}.
+            return getStringOfFirstDayOfWeek(
+                    java.util.Calendar.getInstance(locale).getFirstDayOfWeek());
+        }
+
+        @DoNotInline
+        static Locale getDefaultLocale() {
+            return Locale.getDefault(Category.FORMAT);
+        }
+
+        private static String getStringOfFirstDayOfWeek(int fw) {
+            String[] arrDays = {
+                    FirstDayOfWeek.SUNDAY,
+                    FirstDayOfWeek.MONDAY,
+                    FirstDayOfWeek.TUESDAY,
+                    FirstDayOfWeek.WEDNESDAY,
+                    FirstDayOfWeek.THURSDAY,
+                    FirstDayOfWeek.FRIDAY,
+                    FirstDayOfWeek.SATURDAY};
+
+            return fw >= 1 && fw <= 7 ? arrDays[fw - 1] : FirstDayOfWeek.DEFAULT;
+        }
+
+        @HourCycle.HourCycleTypes
+        private static String getHourCycleType(
+                DateFormat.HourCycle hourCycle) {
+            switch (hourCycle) {
+                case HOUR_CYCLE_11:
+                    return HourCycle.H11;
+                case HOUR_CYCLE_12:
+                    return HourCycle.H12;
+                case HOUR_CYCLE_23:
+                    return HourCycle.H23;
+                case HOUR_CYCLE_24:
+                    return HourCycle.H24;
+                default:
+                    return HourCycle.DEFAULT;
+            }
+        }
+
+        @TemperatureUnit.TemperatureUnits
+        private static String getResolvedTemperatureUnit(@NonNull Locale locale) {
+            LocalizedNumberFormatter nf = NumberFormatter.with()
+                    .usage("temperature")
+                    .unit(MeasureUnit.CELSIUS)
+                    .locale(locale);
+            return nf.format(1).getOutputUnit().getIdentifier();
+        }
+
+        private Api33Impl() {
+        }
+    }
+
+    private LocalePreferences() {
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
index a0d31d1..688e887f 100644
--- a/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/VelocityTrackerCompat.java
@@ -16,18 +16,36 @@
 
 package androidx.core.view;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.os.Build;
+import android.view.MotionEvent;
 import android.view.VelocityTracker;
 
-/**
- * Helper for accessing features in {@link VelocityTracker}.
- *
- * @deprecated Use {@link VelocityTracker} directly.
- */
-@Deprecated
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+
+/** Helper for accessing features in {@link VelocityTracker}. */
 public final class VelocityTrackerCompat {
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @Retention(SOURCE)
+    @IntDef(value = {
+            MotionEvent.AXIS_X,
+            MotionEvent.AXIS_Y,
+            MotionEvent.AXIS_SCROLL
+    })
+    public @interface VelocityTrackableMotionEventAxis {}
     /**
      * Call {@link VelocityTracker#getXVelocity(int)}.
-     * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device,
+     * If running on a pre-{@link Build.VERSION_CODES#HONEYCOMB} device,
      * returns {@link VelocityTracker#getXVelocity()}.
      *
      * @deprecated Use {@link VelocityTracker#getXVelocity(int)} directly.
@@ -39,7 +57,7 @@
 
     /**
      * Call {@link VelocityTracker#getYVelocity(int)}.
-     * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device,
+     * If running on a pre-{@link Build.VERSION_CODES#HONEYCOMB} device,
      * returns {@link VelocityTracker#getYVelocity()}.
      *
      * @deprecated Use {@link VelocityTracker#getYVelocity(int)} directly.
@@ -49,5 +67,119 @@
         return tracker.getYVelocity(pointerId);
     }
 
+    /**
+     * Checks whether a given velocity-trackable {@link MotionEvent} axis is supported for velocity
+     * tracking by this {@link VelocityTracker} instance (refer to
+     * {@link #getAxisVelocity(VelocityTracker, int, int)} for a list of potentially
+     * velocity-trackable axes).
+     *
+     * <p>Note that the value returned from this method will stay the same for a given instance, so
+     * a single check for axis support is enough per a {@link VelocityTracker} instance.
+     *
+     * @param tracker The {@link VelocityTracker} for which to check axis support.
+     * @param axis The axis to check for velocity support.
+     * @return {@code true} if {@code axis} is supported for velocity tracking, or {@code false}
+     *         otherwise.
+     * @see #getAxisVelocity(VelocityTracker, int, int)
+     * @see #getAxisVelocity(VelocityTracker, int)
+     */
+    public static boolean isAxisSupported(@NonNull VelocityTracker tracker,
+            @VelocityTrackableMotionEventAxis int axis) {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return Api34Impl.isAxisSupported(tracker, axis);
+        }
+        return axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y;
+    }
+
+    /**
+     * Equivalent to calling {@link #getAxisVelocity(VelocityTracker, int, int)} for {@code axis}
+     * and the active pointer.
+     *
+     * @param tracker The {@link VelocityTracker} from which to get axis velocity.
+     * @param axis Which axis' velocity to return.
+     * @return The previously computed velocity for {@code axis} for the active pointer if
+     *         {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not
+     *         supported for the axis.
+     * @see #isAxisSupported(VelocityTracker, int)
+     * @see #getAxisVelocity(VelocityTracker, int, int)
+     */
+    public static float getAxisVelocity(@NonNull VelocityTracker tracker,
+            @VelocityTrackableMotionEventAxis int axis) {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return Api34Impl.getAxisVelocity(tracker, axis);
+        }
+        if (axis == MotionEvent.AXIS_X) {
+            return tracker.getXVelocity();
+        }
+        if (axis == MotionEvent.AXIS_Y) {
+            return tracker.getYVelocity();
+        }
+        return  0;
+    }
+
+    /**
+     * Retrieve the last computed velocity for a given motion axis. You must first call
+     * {@link VelocityTracker#computeCurrentVelocity(int)} or
+     * {@link VelocityTracker#computeCurrentVelocity(int, float)} before calling this function.
+     *
+     * <p>In addition to {@link MotionEvent#AXIS_X} and {@link MotionEvent#AXIS_Y} which have been
+     * supported since the introduction of this class, the following axes can be candidates for this
+     * method:
+     * <ul>
+     *   <li> {@link MotionEvent#AXIS_SCROLL}: supported starting
+     *        {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+     * </ul>
+     *
+     * <p>Before accessing velocities of an axis using this method, check that your
+     * {@link VelocityTracker} instance supports the axis by using
+     * {@link #isAxisSupported(VelocityTracker, int)}.
+     *
+     * @param tracker The {@link VelocityTracker} from which to get axis velocity.
+     * @param axis Which axis' velocity to return.
+     * @param pointerId Which pointer's velocity to return.
+     * @return The previously computed velocity for {@code axis} for pointer ID of {@code id} if
+     *         {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not
+     *         supported for the axis.
+     * @see #isAxisSupported(VelocityTracker, int)
+     */
+    public static float getAxisVelocity(
+            @NonNull VelocityTracker tracker,
+            @VelocityTrackableMotionEventAxis int axis,
+            int pointerId) {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return Api34Impl.getAxisVelocity(tracker, axis, pointerId);
+        }
+        if (axis == MotionEvent.AXIS_X) {
+            return tracker.getXVelocity(pointerId);
+        }
+        if (axis == MotionEvent.AXIS_Y) {
+            return tracker.getYVelocity(pointerId);
+        }
+        return  0;
+
+    }
+
+    @RequiresApi(34)
+    private static class Api34Impl {
+        private Api34Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static boolean isAxisSupported(VelocityTracker velocityTracker, int axis) {
+            return velocityTracker.isAxisSupported(axis);
+        }
+
+        @DoNotInline
+        static float getAxisVelocity(VelocityTracker velocityTracker, int axis, int id) {
+            return velocityTracker.getAxisVelocity(axis, id);
+        }
+
+        @DoNotInline
+        static float getAxisVelocity(VelocityTracker velocityTracker, int axis) {
+            return velocityTracker.getAxisVelocity(axis);
+        }
+    }
+
     private VelocityTrackerCompat() {}
 }
diff --git a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
index 511d9b8..91586a5 100644
--- a/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityWindowInfoCompat.java
@@ -87,6 +87,25 @@
         return null;
     }
 
+    /**
+     * Creates a new AccessibilityWindowInfoCompat.
+     * <p>
+     * Compatibility:
+     *  <ul>
+     *      <li>Api &lt; 30: Will not wrap an
+     *      {@link android.view.accessibility.AccessibilityWindowInfo} instance.</li>
+     *  </ul>
+     * </p>
+     *
+     */
+    public AccessibilityWindowInfoCompat() {
+        if (SDK_INT >= 30) {
+            mInfo = Api30Impl.instantiateAccessibilityWindowInfo();
+        } else {
+            mInfo = null;
+        }
+    }
+
     private AccessibilityWindowInfoCompat(Object info) {
         mInfo = info;
     }
@@ -541,6 +560,18 @@
         }
     }
 
+    @RequiresApi(30)
+    private static class Api30Impl {
+        private Api30Impl() {
+            // This class is non instantiable.
+        }
+
+        @DoNotInline
+        static AccessibilityWindowInfo instantiateAccessibilityWindowInfo() {
+            return new AccessibilityWindowInfo();
+        }
+    }
+
     @RequiresApi(33)
     private static class Api33Impl {
         private Api33Impl() {
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index cd03d32..3ac46f0 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -44,23 +44,23 @@
   }
 
   public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -68,8 +68,8 @@
   public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
     ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
-    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
   }
@@ -154,23 +154,23 @@
   }
 
   public final class GetPublicKeyCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean allowHybrid);
+    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOption(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -178,8 +178,8 @@
   public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
     ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
-    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
   }
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index cd03d32..3ac46f0 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -44,23 +44,23 @@
   }
 
   public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -68,8 +68,8 @@
   public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
     ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
-    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
   }
@@ -154,23 +154,23 @@
   }
 
   public final class GetPublicKeyCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean allowHybrid);
+    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOption(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -178,8 +178,8 @@
   public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
     ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
-    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
   }
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index cd03d32..3ac46f0 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -44,23 +44,23 @@
   }
 
   public final class CreatePublicKeyCredentialRequest extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequest(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequest(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class CreatePublicKeyCredentialRequestPrivileged extends androidx.credentials.CreateCredentialRequest {
-    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public CreatePublicKeyCredentialRequestPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -68,8 +68,8 @@
   public static final class CreatePublicKeyCredentialRequestPrivileged.Builder {
     ctor public CreatePublicKeyCredentialRequestPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged build();
-    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.Builder setRequestJson(String requestJson);
   }
@@ -154,23 +154,23 @@
   }
 
   public final class GetPublicKeyCredentialOption extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean allowHybrid);
+    ctor public GetPublicKeyCredentialOption(String requestJson, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOption(String requestJson);
-    method public boolean getAllowHybrid();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String requestJson;
   }
 
   public final class GetPublicKeyCredentialOptionPrivileged extends androidx.credentials.GetCredentialOption {
-    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean allowHybrid);
+    ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash, optional boolean preferImmediatelyAvailableCredentials);
     ctor public GetPublicKeyCredentialOptionPrivileged(String requestJson, String relyingParty, String clientDataHash);
-    method public boolean getAllowHybrid();
     method public String getClientDataHash();
+    method public boolean getPreferImmediatelyAvailableCredentials();
     method public String getRelyingParty();
     method public String getRequestJson();
-    property public final boolean allowHybrid;
     property public final String clientDataHash;
+    property public final boolean preferImmediatelyAvailableCredentials;
     property public final String relyingParty;
     property public final String requestJson;
   }
@@ -178,8 +178,8 @@
   public static final class GetPublicKeyCredentialOptionPrivileged.Builder {
     ctor public GetPublicKeyCredentialOptionPrivileged.Builder(String requestJson, String relyingParty, String clientDataHash);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged build();
-    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setAllowHybrid(boolean allowHybrid);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setClientDataHash(String clientDataHash);
+    method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRelyingParty(String relyingParty);
     method public androidx.credentials.GetPublicKeyCredentialOptionPrivileged.Builder setRequestJson(String requestJson);
   }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index c7abdcb..80b7d09 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -16,7 +16,7 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_REQUEST_JSON;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -52,28 +52,31 @@
     }
 
     @Test
-    public void constructor_success()  {
+    public void constructor_success() {
         new CreatePublicKeyCredentialRequest(
                 "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault()  {
+    public void constructor_setsPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
                 new CreatePublicKeyCredentialRequest(
                         "JSON");
-        boolean allowHybridActual = createPublicKeyCredentialRequest.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse()  {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         CreatePublicKeyCredentialRequest createPublicKeyCredentialRequest =
                 new CreatePublicKeyCredentialRequest("testJson",
-                        allowHybridExpected);
-        boolean allowHybridActual = createPublicKeyCredentialRequest.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
@@ -89,7 +92,7 @@
     @Test
     public void getter_frameworkProperties_success() {
         String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -98,10 +101,11 @@
         expectedData.putString(
                 BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
         expectedData.putBoolean(
-                BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
-        CreatePublicKeyCredentialRequest request =
-                new CreatePublicKeyCredentialRequest(requestJsonExpected, allowHybridExpected);
+        CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(
+                requestJsonExpected, preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
         assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedData)).isTrue();
@@ -123,6 +127,7 @@
         CreatePublicKeyCredentialRequest convertedSubclassRequest =
                 (CreatePublicKeyCredentialRequest) convertedRequest;
         assertThat(convertedSubclassRequest.getRequestJson()).isEqualTo(request.getRequestJson());
-        assertThat(convertedSubclassRequest.allowHybrid()).isEqualTo(request.allowHybrid());
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials())
+                .isEqualTo(request.preferImmediatelyAvailableCredentials());
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
index 623ae8d..bc684202 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
@@ -16,7 +16,7 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
 import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY;
 import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_REQUEST_JSON;
@@ -47,43 +47,49 @@
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault() {
+    public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged(
                         "JSON", "relyingParty", "HASH");
-        boolean allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse() {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged("JSON",
                         "relyingParty",
                         "HASH",
-                        allowHybridExpected);
-        boolean allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
-    public void builder_build_defaultAllowHybrid_true() {
+    public void builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
         CreatePublicKeyCredentialRequestPrivileged defaultPrivilegedRequest = new
                 CreatePublicKeyCredentialRequestPrivileged.Builder("{\"Data\":5}",
                 "relyingParty", "HASH").build();
-        assertThat(defaultPrivilegedRequest.allowHybrid()).isTrue();
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials()).isFalse();
     }
 
     @Test
-    public void builder_build_nonDefaultAllowHybrid_false() {
-        boolean allowHybridExpected = false;
+    public void builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged.Builder("JSON",
                         "relyingParty", "HASH")
-                        .setAllowHybrid(allowHybridExpected).build();
-        boolean allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        .setPreferImmediatelyAvailableCredentials(
+                                preferImmediatelyAvailableCredentialsExpected).build();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
@@ -98,14 +104,14 @@
 
     @Test
     public void getter_relyingParty_success() {
-        String testrelyingPartyExpected = "relyingParty";
+        String testRelyingPartyExpected = "relyingParty";
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged(
                         "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                        testrelyingPartyExpected, "X342%4dfd7&");
-        String testrelyingPartyActual = createPublicKeyCredentialRequestPrivileged
+                        testRelyingPartyExpected, "X342%4dfd7&");
+        String testRelyingPartyActual = createPublicKeyCredentialRequestPrivileged
                 .getRelyingParty();
-        assertThat(testrelyingPartyActual).isEqualTo(testrelyingPartyExpected);
+        assertThat(testRelyingPartyActual).isEqualTo(testRelyingPartyExpected);
     }
 
     @Test
@@ -114,7 +120,7 @@
         CreatePublicKeyCredentialRequestPrivileged createPublicKeyCredentialRequestPrivileged =
                 new CreatePublicKeyCredentialRequestPrivileged(
                         "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                         "relyingParty", clientDataHashExpected);
+                        "relyingParty", clientDataHashExpected);
         String clientDataHashActual =
                 createPublicKeyCredentialRequestPrivileged.getClientDataHash();
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected);
@@ -125,7 +131,7 @@
         String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
         String relyingPartyExpected = "relyingParty";
         String clientDataHashExpected = "X342%4dfd7&";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -134,12 +140,14 @@
         expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
         expectedData.putString(BUNDLE_KEY_RELYING_PARTY, relyingPartyExpected);
         expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
-        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
         CreatePublicKeyCredentialRequestPrivileged request =
                 new CreatePublicKeyCredentialRequestPrivileged(
                         requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
-                        allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
         assertThat(TestUtilsKt.equals(request.getCredentialData(), expectedData)).isTrue();
@@ -165,6 +173,7 @@
         assertThat(convertedSubclassRequest.getRelyingParty()).isEqualTo(request.getRelyingParty());
         assertThat(convertedSubclassRequest.getClientDataHash())
                 .isEqualTo(request.getClientDataHash());
-        assertThat(convertedSubclassRequest.allowHybrid()).isEqualTo(request.allowHybrid());
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials()).isEqualTo(
+                request.preferImmediatelyAvailableCredentials());
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
index d766191..aaa4e41 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
@@ -40,52 +40,63 @@
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged(
             "JSON", "RelyingParty", "HASH"
         )
-        val allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridToFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged(
             "testJson",
-            "RelyingParty", "Hash", allowHybridExpected
+            "RelyingParty", "Hash", preferImmediatelyAvailableCredentialsExpected
         )
-        val allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
-    fun builder_build_defaultAllowHybrid_true() {
+    fun builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
         val defaultPrivilegedRequest = CreatePublicKeyCredentialRequestPrivileged.Builder(
             "{\"Data\":5}",
             "RelyingParty", "HASH"
         ).build()
-        assertThat(defaultPrivilegedRequest.allowHybrid).isTrue()
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials).isFalse()
     }
 
     @Test
-    fun builder_build_nonDefaultAllowHybrid_false() {
-        val allowHybridExpected = false
+    fun builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val createPublicKeyCredentialRequestPrivileged = CreatePublicKeyCredentialRequestPrivileged
             .Builder(
                 "testJson",
                 "RelyingParty", "Hash"
-            ).setAllowHybrid(allowHybridExpected).build()
-        val allowHybridActual = createPublicKeyCredentialRequestPrivileged.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+            )
+            .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentialsExpected)
+            .build()
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequestPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
     fun getter_requestJson_success() {
         val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
         val createPublicKeyCredentialReqPriv =
-            CreatePublicKeyCredentialRequestPrivileged(testJsonExpected, "RelyingParty",
-                "HASH")
+            CreatePublicKeyCredentialRequestPrivileged(
+                testJsonExpected, "RelyingParty",
+                "HASH"
+            )
         val testJsonActual = createPublicKeyCredentialReqPriv.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
@@ -119,7 +130,7 @@
         val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
         val relyingPartyExpected = "RelyingParty"
         val clientDataHashExpected = "X342%4dfd7&"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -130,22 +141,24 @@
             CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_REQUEST_JSON,
             requestJsonExpected
         )
-        expectedData.putString(CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY,
-            relyingPartyExpected)
+        expectedData.putString(
+            CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RELYING_PARTY,
+            relyingPartyExpected
+        )
         expectedData.putString(
             CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
             clientDataHashExpected
         )
         expectedData.putBoolean(
-            CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID,
-            allowHybridExpected
+            CreatePublicKeyCredentialRequest.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         val request = CreatePublicKeyCredentialRequestPrivileged(
             requestJsonExpected,
             relyingPartyExpected,
             clientDataHashExpected,
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
@@ -174,6 +187,7 @@
         assertThat(convertedSubclassRequest.relyingParty).isEqualTo(request.relyingParty)
         assertThat(convertedSubclassRequest.clientDataHash)
             .isEqualTo(request.clientDataHash)
-        assertThat(convertedSubclassRequest.allowHybrid).isEqualTo(request.allowHybrid)
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
+            .isEqualTo(request.preferImmediatelyAvailableCredentials)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index d133451..4f74809 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -18,8 +18,8 @@
 
 import android.os.Bundle
 import androidx.credentials.CreateCredentialRequest.Companion.createFrom
-import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_ALLOW_HYBRID
 import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_REQUEST_JSON
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -47,23 +47,26 @@
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
             "JSON"
         )
-        val allowHybridActual = createPublicKeyCredentialRequest.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridToFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val createPublicKeyCredentialRequest = CreatePublicKeyCredentialRequest(
             "testJson",
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
-        val allowHybridActual = createPublicKeyCredentialRequest.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            createPublicKeyCredentialRequest.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual)
+            .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
     }
 
     @Test
@@ -77,7 +80,7 @@
     @Test
     fun getter_frameworkProperties_success() {
         val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -88,12 +91,13 @@
             BUNDLE_KEY_REQUEST_JSON, requestJsonExpected
         )
         expectedData.putBoolean(
-            BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected
+            BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         val request = CreatePublicKeyCredentialRequest(
             requestJsonExpected,
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
@@ -116,6 +120,7 @@
         )
         val convertedSubclassRequest = convertedRequest as CreatePublicKeyCredentialRequest
         assertThat(convertedSubclassRequest.requestJson).isEqualTo(request.requestJson)
-        assertThat(convertedSubclassRequest.allowHybrid).isEqualTo(request.allowHybrid)
+        assertThat(convertedSubclassRequest.preferImmediatelyAvailableCredentials)
+            .isEqualTo(request.preferImmediatelyAvailableCredentials)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
index dad1fe3..7b78eb0 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
@@ -16,7 +16,7 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -53,28 +53,31 @@
     }
 
     @Test
-    public void constructor_success()  {
+    public void constructor_success() {
         new GetPublicKeyCredentialOption(
-                        "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
+                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault() {
+    public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
                 new GetPublicKeyCredentialOption(
                         "JSON");
-        boolean allowHybridActual = getPublicKeyCredentialOpt.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse() {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsToTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         GetPublicKeyCredentialOption getPublicKeyCredentialOpt =
                 new GetPublicKeyCredentialOption(
-                        "JSON", allowHybridExpected);
-        boolean allowHybridActual = getPublicKeyCredentialOpt.allowHybrid();
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected);
+                        "JSON", preferImmediatelyAvailableCredentialsExpected);
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
@@ -89,16 +92,18 @@
     @Test
     public void getter_frameworkProperties_success() {
         String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
                 GetPublicKeyCredentialOption.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION);
         expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
-        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
-        GetPublicKeyCredentialOption option =
-                new GetPublicKeyCredentialOption(requestJsonExpected, allowHybridExpected);
+        GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(
+                requestJsonExpected, preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
         assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
@@ -119,6 +124,7 @@
         GetPublicKeyCredentialOption convertedSubclassOption =
                 (GetPublicKeyCredentialOption) convertedOption;
         assertThat(convertedSubclassOption.getRequestJson()).isEqualTo(option.getRequestJson());
-        assertThat(convertedSubclassOption.allowHybrid()).isEqualTo(option.allowHybrid());
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials()).isEqualTo(
+                option.preferImmediatelyAvailableCredentials());
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
index 760eead..4d6d6fd 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
@@ -16,8 +16,8 @@
 
 package androidx.credentials;
 
-import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_ALLOW_HYBRID;
 import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
+import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS;
 import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY;
 import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON;
 
@@ -42,46 +42,56 @@
     @Test
     public void constructor_success() {
         new GetPublicKeyCredentialOptionPrivileged(
-                        "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
-                        "RelyingParty", "ClientDataHash");
+                "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}",
+                "RelyingParty", "ClientDataHash");
     }
 
     @Test
-    public void constructor_setsAllowHybridToTrueByDefault() {
+    public void constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
                 new GetPublicKeyCredentialOptionPrivileged(
                         "JSON", "RelyingParty", "HASH");
-        boolean allowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid();
-        assertThat(allowHybridActual).isTrue();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse();
     }
 
     @Test
-    public void constructor_setsAllowHybridToFalse() {
-        boolean allowHybridExpected = false;
+    public void constructor_setPreferImmediatelyAvailableCredentialsTrue() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
-                new GetPublicKeyCredentialOptionPrivileged("testJson",
-                        "RelyingParty", "Hash", allowHybridExpected);
-        boolean getAllowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid();
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected);
+                new GetPublicKeyCredentialOptionPrivileged(
+                        "testJson",
+                        "RelyingParty",
+                        "Hash",
+                        preferImmediatelyAvailableCredentialsExpected
+                );
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
-    public void builder_build_defaultAllowHybrid_success() {
+    public void builder_build_defaultPreferImmediatelyAvailableCredentials_success() {
         GetPublicKeyCredentialOptionPrivileged defaultPrivilegedRequest = new
                 GetPublicKeyCredentialOptionPrivileged.Builder("{\"Data\":5}",
                 "RelyingParty", "HASH").build();
-        assertThat(defaultPrivilegedRequest.allowHybrid()).isTrue();
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials()).isFalse();
     }
 
     @Test
-    public void builder_build_nonDefaultAllowHybrid_success() {
-        boolean allowHybridExpected = false;
+    public void builder_build_nonDefaultPreferImmediatelyAvailableCredentials_success() {
+        boolean preferImmediatelyAvailableCredentialsExpected = true;
         GetPublicKeyCredentialOptionPrivileged getPublicKeyCredentialOptionPrivileged =
                 new GetPublicKeyCredentialOptionPrivileged.Builder("testJson",
                         "RelyingParty", "Hash")
-                        .setAllowHybrid(allowHybridExpected).build();
-        boolean getAllowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid();
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected);
+                        .setPreferImmediatelyAvailableCredentials(
+                                preferImmediatelyAvailableCredentialsExpected).build();
+        boolean preferImmediatelyAvailableCredentialsActual =
+                getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials();
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+                preferImmediatelyAvailableCredentialsExpected);
     }
 
     @Test
@@ -121,7 +131,7 @@
         String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
         String relyingPartyExpected = "RelyingParty";
         String clientDataHashExpected = "X342%4dfd7&";
-        boolean allowHybridExpected = false;
+        boolean preferImmediatelyAvailableCredentialsExpected = false;
         Bundle expectedData = new Bundle();
         expectedData.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -130,12 +140,14 @@
         expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
         expectedData.putString(BUNDLE_KEY_RELYING_PARTY, relyingPartyExpected);
         expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
-        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentialsExpected);
 
         GetPublicKeyCredentialOptionPrivileged option =
                 new GetPublicKeyCredentialOptionPrivileged(
                         requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
-                        allowHybridExpected);
+                        preferImmediatelyAvailableCredentialsExpected);
 
         assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
         assertThat(TestUtilsKt.equals(option.getRequestData(), expectedData)).isTrue();
@@ -156,7 +168,8 @@
         GetPublicKeyCredentialOptionPrivileged convertedSubclassOption =
                 (GetPublicKeyCredentialOptionPrivileged) convertedOption;
         assertThat(convertedSubclassOption.getRequestJson()).isEqualTo(option.getRequestJson());
-        assertThat(convertedSubclassOption.allowHybrid()).isEqualTo(option.allowHybrid());
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials()).isEqualTo(
+                option.preferImmediatelyAvailableCredentials());
         assertThat(convertedSubclassOption.getClientDataHash())
                 .isEqualTo(option.getClientDataHash());
         assertThat(convertedSubclassOption.getRelyingParty()).isEqualTo(option.getRelyingParty());
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
index dec9de2..ba2d208 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
@@ -40,51 +40,65 @@
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged(
             "JSON", "RelyingParty", "HASH"
         )
-        val allowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val getPublicKeyCredentialOptPriv = GetPublicKeyCredentialOptionPrivileged(
-            "JSON", "RelyingParty", "HASH", allowHybridExpected
+            "JSON",
+            "RelyingParty",
+            "HASH",
+            preferImmediatelyAvailableCredentialsExpected
         )
-        val getAllowHybridActual = getPublicKeyCredentialOptPriv.allowHybrid
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOptPriv.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
-    fun builder_build_nonDefaultAllowHybrid_false() {
-        val allowHybridExpected = false
+    fun builder_build_nonDefaultPreferImmediatelyAvailableCredentials_true() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val getPublicKeyCredentialOptionPrivileged = GetPublicKeyCredentialOptionPrivileged
             .Builder(
                 "testJson",
                 "RelyingParty", "Hash",
-            ).setAllowHybrid(allowHybridExpected).build()
-        val getAllowHybridActual = getPublicKeyCredentialOptionPrivileged.allowHybrid
-        assertThat(getAllowHybridActual).isEqualTo(allowHybridExpected)
+            )
+            .setPreferImmediatelyAvailableCredentials(preferImmediatelyAvailableCredentialsExpected)
+            .build()
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOptionPrivileged.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isEqualTo(
+            preferImmediatelyAvailableCredentialsExpected
+        )
     }
 
     @Test
-    fun builder_build_defaultAllowHybrid_true() {
+    fun builder_build_defaultPreferImmediatelyAvailableCredentials_false() {
         val defaultPrivilegedRequest = GetPublicKeyCredentialOptionPrivileged.Builder(
             "{\"Data\":5}",
             "RelyingParty", "HASH"
         ).build()
-        assertThat(defaultPrivilegedRequest.allowHybrid).isTrue()
+        assertThat(defaultPrivilegedRequest.preferImmediatelyAvailableCredentials).isFalse()
     }
 
     @Test
     fun getter_requestJson_success() {
         val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
         val getPublicKeyCredentialOptionPrivileged =
-            GetPublicKeyCredentialOptionPrivileged(testJsonExpected, "RelyingParty",
-                "HASH")
+            GetPublicKeyCredentialOptionPrivileged(
+                testJsonExpected, "RelyingParty",
+                "HASH"
+            )
         val testJsonActual = getPublicKeyCredentialOptionPrivileged.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
@@ -116,7 +130,7 @@
         val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
         val relyingPartyExpected = "RelyingParty"
         val clientDataHashExpected = "X342%4dfd7&"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -127,20 +141,23 @@
             GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON,
             requestJsonExpected
         )
-        expectedData.putString(GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY,
-            relyingPartyExpected)
+        expectedData.putString(
+            GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY,
+            relyingPartyExpected
+        )
         expectedData.putString(
             GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
             clientDataHashExpected
         )
         expectedData.putBoolean(
-            GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_ALLOW_HYBRID,
-            allowHybridExpected
+            GetPublicKeyCredentialOptionPrivileged
+                .BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         val option = GetPublicKeyCredentialOptionPrivileged(
             requestJsonExpected, relyingPartyExpected, clientDataHashExpected,
-            allowHybridExpected
+            preferImmediatelyAvailableCredentialsExpected
         )
 
         assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
@@ -162,7 +179,8 @@
         )
         val convertedSubclassOption = convertedOption as GetPublicKeyCredentialOptionPrivileged
         assertThat(convertedSubclassOption.requestJson).isEqualTo(option.requestJson)
-        assertThat(convertedSubclassOption.allowHybrid).isEqualTo(option.allowHybrid)
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials)
+            .isEqualTo(option.preferImmediatelyAvailableCredentials)
         assertThat(convertedSubclassOption.clientDataHash)
             .isEqualTo(option.clientDataHash)
         assertThat(convertedSubclassOption.relyingParty).isEqualTo(option.relyingParty)
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
index 6693856b..4f5f137 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
@@ -45,22 +45,25 @@
     }
 
     @Test
-    fun constructor_setsAllowHybridToTrueByDefault() {
+    fun constructor_setPreferImmediatelyAvailableCredentialsToFalseByDefault() {
         val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
             "JSON"
         )
-        val allowHybridActual = getPublicKeyCredentialOpt.allowHybrid
-        assertThat(allowHybridActual).isTrue()
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual).isFalse()
     }
 
     @Test
-    fun constructor_setsAllowHybridFalse() {
-        val allowHybridExpected = false
+    fun constructor_setPreferImmediatelyAvailableCredentialsTrue() {
+        val preferImmediatelyAvailableCredentialsExpected = true
         val getPublicKeyCredentialOpt = GetPublicKeyCredentialOption(
-            "JSON", allowHybridExpected
+            "JSON", preferImmediatelyAvailableCredentialsExpected
         )
-        val allowHybridActual = getPublicKeyCredentialOpt.allowHybrid
-        assertThat(allowHybridActual).isEqualTo(allowHybridExpected)
+        val preferImmediatelyAvailableCredentialsActual =
+            getPublicKeyCredentialOpt.preferImmediatelyAvailableCredentials
+        assertThat(preferImmediatelyAvailableCredentialsActual)
+            .isEqualTo(preferImmediatelyAvailableCredentialsExpected)
     }
 
     @Test
@@ -74,7 +77,7 @@
     @Test
     fun getter_frameworkProperties_success() {
         val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
-        val allowHybridExpected = false
+        val preferImmediatelyAvailableCredentialsExpected = false
         val expectedData = Bundle()
         expectedData.putString(
             PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
@@ -85,11 +88,13 @@
             requestJsonExpected
         )
         expectedData.putBoolean(
-            GetPublicKeyCredentialOption.BUNDLE_KEY_ALLOW_HYBRID,
-            allowHybridExpected
+            GetPublicKeyCredentialOption.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+            preferImmediatelyAvailableCredentialsExpected
         )
 
-        val option = GetPublicKeyCredentialOption(requestJsonExpected, allowHybridExpected)
+        val option = GetPublicKeyCredentialOption(
+            requestJsonExpected, preferImmediatelyAvailableCredentialsExpected
+        )
 
         assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
         assertThat(equals(option.requestData, expectedData)).isTrue()
@@ -110,6 +115,7 @@
         )
         val convertedSubclassOption = convertedOption as GetPublicKeyCredentialOption
         assertThat(convertedSubclassOption.requestJson).isEqualTo(option.requestJson)
-        assertThat(convertedSubclassOption.allowHybrid).isEqualTo(option.allowHybrid)
+        assertThat(convertedSubclassOption.preferImmediatelyAvailableCredentials)
+            .isEqualTo(option.preferImmediatelyAvailableCredentials)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
index c78e8b2..7f2fcbf 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
@@ -26,21 +26,21 @@
  *
  * @property requestJson the privileged request in JSON format in the standard webauthn web json
  * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (default) otherwise
  * @throws NullPointerException If [requestJson] is null
  * @throws IllegalArgumentException If [requestJson] is empty
  */
 class CreatePublicKeyCredentialRequest @JvmOverloads constructor(
     val requestJson: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean = false
 ) : CreateCredentialRequest(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    credentialData = toCredentialDataBundle(requestJson, allowHybrid),
+    credentialData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
     // The whole request data should be passed during the query phase.
-    candidateQueryData = toCredentialDataBundle(requestJson, allowHybrid),
+    candidateQueryData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
     requireSystemProvider = false,
 ) {
 
@@ -51,19 +51,24 @@
     /** @hide */
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
         internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
         @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
         const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
 
         @JvmStatic
-        internal fun toCredentialDataBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+        internal fun toCredentialDataBundle(
+            requestJson: String,
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Bundle {
             val bundle = Bundle()
             bundle.putString(BUNDLE_KEY_SUBTYPE,
                 BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials)
             return bundle
         }
 
@@ -73,8 +78,10 @@
         internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
-                return CreatePublicKeyCredentialRequest(requestJson!!, (allowHybrid!!) as Boolean)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                return CreatePublicKeyCredentialRequest(requestJson!!,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean)
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
             }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
index 1600894..8d3607d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
@@ -28,28 +28,35 @@
  *
  * @property requestJson the privileged request in JSON format in the standard webauthn web json
  * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson], where
- * rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available passkey registration offering instead of falling back to
+ * discovering remote options, and false (default) otherwise
+ * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
+ * where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
  * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
  * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
  * null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is empty
+ * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
+ * empty
  */
 class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor(
     val requestJson: String,
     val relyingParty: String,
     val clientDataHash: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean = false
 ) : CreateCredentialRequest(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    credentialData = toCredentialDataBundle(requestJson, relyingParty, clientDataHash, allowHybrid),
+    credentialData = toCredentialDataBundle(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials
+    ),
     // The whole request data should be passed during the query phase.
     candidateQueryData = toCredentialDataBundle(
-        requestJson, relyingParty, clientDataHash, allowHybrid),
+        requestJson, relyingParty, clientDataHash, preferImmediatelyAvailableCredentials
+    ),
     requireSystemProvider = false,
 ) {
 
@@ -64,9 +71,9 @@
         private var requestJson: String,
         private var relyingParty: String,
         private var clientDataHash: String
-        ) {
+    ) {
 
-        private var allowHybrid: Boolean = true
+        private var preferImmediatelyAvailableCredentials: Boolean = false
 
         /**
          * Sets the privileged request in JSON format.
@@ -77,11 +84,17 @@
         }
 
         /**
-         * Sets whether hybrid credentials are allowed to fulfill this request, true by default.
+         * Sets to true if you prefer the operation to return immediately when there is no available
+         * passkey registration offering instead of falling back to discovering remote options, and
+         * false otherwise.
+         *
+         * The default value is false.
          */
         @Suppress("MissingGetterMatchingBuilder")
-        fun setAllowHybrid(allowHybrid: Boolean): Builder {
-            this.allowHybrid = allowHybrid
+        fun setPreferImmediatelyAvailableCredentials(
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Builder {
+            this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
             return this
         }
 
@@ -103,8 +116,10 @@
 
         /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
         fun build(): CreatePublicKeyCredentialRequestPrivileged {
-            return CreatePublicKeyCredentialRequestPrivileged(this.requestJson,
-                this.relyingParty, this.clientDataHash, this.allowHybrid)
+            return CreatePublicKeyCredentialRequestPrivileged(
+                this.requestJson,
+                this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
+            )
         }
     }
 
@@ -112,13 +127,18 @@
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
         const val BUNDLE_KEY_RELYING_PARTY = "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
+
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
         const val BUNDLE_KEY_CLIENT_DATA_HASH =
             "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
+
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
         const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+
         @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
         const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
@@ -129,7 +149,7 @@
             requestJson: String,
             relyingParty: String,
             clientDataHash: String,
-            allowHybrid: Boolean
+            preferImmediatelyAvailableCredentials: Boolean
         ): Bundle {
             val bundle = Bundle()
             bundle.putString(
@@ -139,24 +159,28 @@
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
             bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
             bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials
+            )
             return bundle
         }
 
         @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-                                         // boolean value from being returned.
+        // boolean value from being returned.
         @JvmStatic
         internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
                 val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
                 val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
                 return CreatePublicKeyCredentialRequestPrivileged(
                     requestJson!!,
                     rp!!,
                     clientDataHash!!,
-                    (allowHybrid!!) as Boolean,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean,
                 )
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index 2896622..3417c54 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -42,11 +42,16 @@
          * Post-U, providers will be registered with the framework, and enabled by the user.
          */
         fun getBestAvailableProvider(context: Context): CredentialProvider? {
-            if (Build.VERSION.SDK_INT <= MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) {
+            if ((Build.VERSION.SDK_INT <= MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) &&
+                (Build.VERSION.PREVIEW_SDK_INT == 0)) {
                 return tryCreatePreUOemProvider(context)
+            } else if ((Build.VERSION.SDK_INT == MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) &&
+                (Build.VERSION.PREVIEW_SDK_INT > 0)) {
+                return CredentialProviderFrameworkImpl(context)
+            } else if (Build.VERSION.SDK_INT > MAX_CRED_MAN_PRE_FRAMEWORK_API_LEVEL) {
+                return CredentialProviderFrameworkImpl(context)
             } else {
-                // TODO("Implement")
-                throw UnsupportedOperationException("Post-U not supported yet")
+                return null
             }
         }
 
@@ -84,8 +89,10 @@
         @Suppress("deprecation")
         private fun getAllowedProvidersFromManifest(context: Context): List<String> {
             val packageInfo = context.packageManager
-                .getPackageInfo(context.packageName, PackageManager.GET_META_DATA or
-                        PackageManager.GET_SERVICES)
+                .getPackageInfo(
+                    context.packageName, PackageManager.GET_META_DATA or
+                        PackageManager.GET_SERVICES
+                )
 
             val classNames = mutableListOf<String>()
             if (packageInfo.services != null) {
@@ -101,4 +108,4 @@
             return classNames.toList()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
new file mode 100644
index 0000000..117799b
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFrameworkImpl.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.credentials
+
+import android.app.Activity
+import android.content.Context
+import android.credentials.CredentialManager
+import android.os.CancellationSignal
+import android.os.OutcomeReceiver
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.credentials.exceptions.ClearCredentialException
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Framework credential provider implementation that allows credential
+ * manager requests to be routed to the framework.
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class CredentialProviderFrameworkImpl(context: Context) : CredentialProvider {
+    private val credentialManager: CredentialManager =
+        context.getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
+
+    override fun onGetCredential(
+        request: GetCredentialRequest,
+        activity: Activity,
+        cancellationSignal: CancellationSignal?,
+        executor: Executor,
+        callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>
+    ) {
+        Log.i(TAG, "In CredentialProviderFrameworkImpl onGetCredential")
+
+        val outcome = object : OutcomeReceiver<
+            android.credentials.GetCredentialResponse, android.credentials.GetCredentialException> {
+            override fun onResult(response: android.credentials.GetCredentialResponse) {
+                Log.i(TAG, "GetCredentialResponse returned from framework")
+                callback.onResult(convertGetResponseToJetpackClass(response))
+            }
+            override fun onError(error: android.credentials.GetCredentialException) {
+                Log.i(TAG, "GetCredentialResponse error returned from framework")
+                // TODO("Covert to the appropriate exception")
+                callback.onError(GetCredentialUnknownException(error.message))
+            }
+        }
+        credentialManager.executeGetCredential(
+            convertGetRequestToFrameworkClass(request),
+            activity,
+            cancellationSignal,
+            Executors.newSingleThreadExecutor(),
+            outcome)
+    }
+
+    override fun onCreateCredential(
+        request: CreateCredentialRequest,
+        activity: Activity,
+        cancellationSignal: CancellationSignal?,
+        executor: Executor,
+        callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>
+    ) {
+        Log.i(TAG, "In CredentialProviderFrameworkImpl onCreateCredential")
+
+        val outcome = object : OutcomeReceiver<
+            android.credentials.CreateCredentialResponse,
+            android.credentials.CreateCredentialException> {
+            override fun onResult(response: android.credentials.CreateCredentialResponse) {
+                Log.i(TAG, "Create Result returned from framework: ")
+                callback.onResult(CreateCredentialResponse.createFrom(
+                    request.type, response.data))
+            }
+
+            override fun onError(error: android.credentials.CreateCredentialException) {
+                Log.i(TAG, "CreateCredentialResponse error returned from framework")
+                // TODO("Covert to the appropriate exception")
+                callback.onError(CreateCredentialUnknownException(error.message))
+            }
+        }
+
+        credentialManager.executeCreateCredential(
+            android.credentials.CreateCredentialRequest(
+                request.type,
+                request.credentialData,
+                request.candidateQueryData,
+                request.requireSystemProvider),
+            activity,
+            cancellationSignal,
+            Executors.newSingleThreadExecutor(),
+            outcome)
+    }
+
+    private fun convertGetRequestToFrameworkClass(request: GetCredentialRequest):
+        android.credentials.GetCredentialRequest {
+        val builder = android.credentials.GetCredentialRequest.Builder()
+        request.getCredentialOptions.forEach {
+            builder.addGetCredentialOption(
+                android.credentials.GetCredentialOption(
+                    it.type, it.requestData, it.candidateQueryData, it.requireSystemProvider
+                ))
+        }
+        return builder.build()
+    }
+
+    internal fun convertGetResponseToJetpackClass(
+        response: android.credentials.GetCredentialResponse
+    ): GetCredentialResponse {
+        // TODO("Look into the response credential being non null")
+        val credential = response.credential!!
+        return GetCredentialResponse(Credential.createFrom(
+            credential.type, credential.data))
+    }
+
+    override fun isAvailableOnDevice(): Boolean {
+        // TODO("Base it on API level check")
+        return true
+    }
+
+    override fun onClearCredential(
+        request: ClearCredentialStateRequest,
+        cancellationSignal: CancellationSignal?,
+        executor: Executor,
+        callback: CredentialManagerCallback<Void?, ClearCredentialException>
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    companion object {
+        private const val TAG = "CredManProvService"
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
index 8a038d6..f65a8b5 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
@@ -17,7 +17,6 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.annotation.VisibleForTesting
 import androidx.credentials.internal.FrameworkClassParsingException
 
 /**
@@ -25,20 +24,20 @@
  *
  * @property requestJson the privileged request in JSON format in the standard webauthn web json
  * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credential instead of falling back to discovering remote
+ * credentials, and false (default) otherwise
  * @throws NullPointerException If [requestJson] is null
  * @throws IllegalArgumentException If [requestJson] is empty
  */
 class GetPublicKeyCredentialOption @JvmOverloads constructor(
     val requestJson: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true,
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean = false,
 ) : GetCredentialOption(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    requestData = toRequestDataBundle(requestJson, allowHybrid),
-    candidateQueryData = toRequestDataBundle(requestJson, allowHybrid),
+    requestData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
+    candidateQueryData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
     requireSystemProvider = false
 ) {
     init {
@@ -47,23 +46,25 @@
 
     /** @hide */
     companion object {
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-        const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
+        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+        internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
 
         @JvmStatic
-        internal fun toRequestDataBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+        internal fun toRequestDataBundle(
+            requestJson: String,
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Bundle {
             val bundle = Bundle()
             bundle.putString(
                 PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
                 BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
             )
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials)
             return bundle
         }
 
@@ -73,8 +74,10 @@
         internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
-                return GetPublicKeyCredentialOption(requestJson!!, (allowHybrid!!) as Boolean)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                return GetPublicKeyCredentialOption(requestJson!!,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean)
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
             }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
index 033cd65..d402e5c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
@@ -17,7 +17,6 @@
 package androidx.credentials
 
 import android.os.Bundle
-import androidx.annotation.VisibleForTesting
 import androidx.credentials.internal.FrameworkClassParsingException
 
 /**
@@ -28,26 +27,37 @@
  *
  * @property requestJson the privileged request in JSON format in the standard webauthn web json
  * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
- * true by default, with hybrid credentials defined
- * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
+ * immediately when there is no available credential instead of falling back to discovering remote
+ * credentials, and false (default) otherwise
  * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
  * where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
  * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
  * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
  * is null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is empty
+ * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
+ * empty
  */
 class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
     val requestJson: String,
     val relyingParty: String,
     val clientDataHash: String,
-    @get:JvmName("allowHybrid")
-    val allowHybrid: Boolean = true
+    @get:JvmName("preferImmediatelyAvailableCredentials")
+    val preferImmediatelyAvailableCredentials: Boolean = false
 ) : GetCredentialOption(
     type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    requestData = toBundle(requestJson, relyingParty, clientDataHash, allowHybrid),
-    candidateQueryData = toBundle(requestJson, relyingParty, clientDataHash, allowHybrid),
+    requestData = toBundle(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials
+    ),
+    candidateQueryData = toBundle(
+        requestJson,
+        relyingParty,
+        clientDataHash,
+        preferImmediatelyAvailableCredentials
+    ),
     requireSystemProvider = false,
 ) {
 
@@ -62,9 +72,9 @@
         private var requestJson: String,
         private var relyingParty: String,
         private var clientDataHash: String
-        ) {
+    ) {
 
-        private var allowHybrid: Boolean = true
+        private var preferImmediatelyAvailableCredentials: Boolean = false
 
         /**
          * Sets the privileged request in JSON format.
@@ -75,11 +85,17 @@
         }
 
         /**
-         * Sets whether hybrid credentials are allowed to fulfill this request, true by default.
+         * Sets to true if you prefer the operation to return immediately when there is no available
+         * credential instead of falling back to discovering remote credentials, and false
+         * otherwise.
+         *
+         * The default value is false.
          */
         @Suppress("MissingGetterMatchingBuilder")
-        fun setAllowHybrid(allowHybrid: Boolean): Builder {
-            this.allowHybrid = allowHybrid
+        fun setPreferImmediatelyAvailableCredentials(
+            preferImmediatelyAvailableCredentials: Boolean
+        ): Builder {
+            this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
             return this
         }
 
@@ -101,24 +117,23 @@
 
         /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
         fun build(): GetPublicKeyCredentialOptionPrivileged {
-            return GetPublicKeyCredentialOptionPrivileged(this.requestJson,
-                this.relyingParty, this.clientDataHash, this.allowHybrid)
+            return GetPublicKeyCredentialOptionPrivileged(
+                this.requestJson,
+                this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
+            )
         }
     }
 
     /** @hide */
     companion object {
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_RELYING_PARTY = "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+        internal const val BUNDLE_KEY_RELYING_PARTY =
+            "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
+        internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
             "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
-        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-        const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
+        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
+            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
+        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+        internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
             "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
                 "_PRIVILEGED"
 
@@ -127,7 +142,7 @@
             requestJson: String,
             relyingParty: String,
             clientDataHash: String,
-            allowHybrid: Boolean
+            preferImmediatelyAvailableCredentials: Boolean
         ): Bundle {
             val bundle = Bundle()
             bundle.putString(
@@ -137,24 +152,28 @@
             bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
             bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
             bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
-            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            bundle.putBoolean(
+                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
+                preferImmediatelyAvailableCredentials
+            )
             return bundle
         }
 
         @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-                                         // boolean value from being returned.
+        // boolean value from being returned.
         @JvmStatic
         internal fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
             try {
                 val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
                 val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
                 val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
-                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                val preferImmediatelyAvailableCredentials =
+                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
                 return GetPublicKeyCredentialOptionPrivileged(
                     requestJson!!,
                     rp!!,
                     clientDataHash!!,
-                    (allowHybrid!!) as Boolean,
+                    (preferImmediatelyAvailableCredentials!!) as Boolean,
                 )
             } catch (e: Exception) {
                 throw FrameworkClassParsingException()
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
new file mode 100644
index 0000000..5279063
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/Action.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import java.util.Collections
+
+/**
+ * An actionable entry that is shown on the user selector. When selected,
+ * the associated [PendingIntent] is invoked.
+ *
+ * See [CredentialsResponseContent] for usage.
+ *
+ * @property title the title to be displayed on the UI with this
+ * action entry
+ * @property subTitle the subTitle to be displayed on the UI with this
+ * action entry
+ * @property pendingIntent the [PendingIntent] to be invoked when user
+ * selects this action entry
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class Action constructor(
+    val title: CharSequence,
+    val subTitle: CharSequence?,
+    val pendingIntent: PendingIntent,
+    ) {
+
+    init {
+        require(title.isNotEmpty()) { "title must not be empty" }
+    }
+
+    companion object {
+        private const val TAG = "Action"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_TITLE =
+            "androidx.credentials.provider.action.HINT_ACTION_TITLE"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_SUBTITLE =
+            "androidx.credentials.provider.action.HINT_ACTION_SUBTEXT"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT"
+
+        @JvmStatic
+        fun toSlice(action: Action): Slice {
+            // TODO("Put the right spec and version value")
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
+                .addText(action.title, /*subType=*/null,
+                    listOf(SLICE_HINT_TITLE))
+                .addText(action.subTitle, /*subType=*/null,
+                    listOf(SLICE_HINT_SUBTITLE))
+            sliceBuilder.addAction(action.pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null)
+            return sliceBuilder.build()
+        }
+
+        /**
+         * Returns an instance of [Action] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): Action? {
+            // TODO("Put the right spec and version value")
+            var title: CharSequence = ""
+            var subTitle: CharSequence? = null
+            var pendingIntent: PendingIntent? = null
+
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_TITLE)) {
+                    title = it.text
+                } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
+                    subTitle = it.text
+                } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+                    pendingIntent = it.action
+                }
+            }
+
+            return try {
+                Action(title, subTitle, pendingIntent!!)
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+
+        @JvmStatic
+        internal fun toFrameworkClass(action: Action): android.service.credentials.Action {
+            return android.service.credentials.Action(toSlice(action), action.pendingIntent)
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
new file mode 100644
index 0000000..4ef04cd
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/AuthenticationAction.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.credentials.provider
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.provider.Action.Companion.toSlice
+import java.util.Collections
+/**
+ * An entry on the selector, denoting that authentication is needed to proceed.
+ *
+ * Providers should set this entry when the provider app is locked, and no credentials can
+ * be returned. Providers must set the [PendingIntent] that leads to their authentication flow.
+ *
+ * @property pendingIntent the [PendingIntent] to be invoked when the user selects
+ * this authentication entry
+ *
+ * See [CredentialsResponseContent] for usage details.
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class AuthenticationAction constructor(
+    val pendingIntent: PendingIntent,
+) {
+    companion object {
+        private const val TAG = "AuthenticationAction"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"
+        @JvmStatic
+        fun toSlice(authenticationAction: AuthenticationAction): Slice {
+            // TODO("Put the right spec and version value")
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
+            sliceBuilder.addAction(authenticationAction.pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null)
+            return sliceBuilder.build()
+        }
+        @JvmStatic
+        internal fun toFrameworkClass(authenticationAction: AuthenticationAction):
+            android.service.credentials.Action {
+            return android.service.credentials.Action(toSlice(authenticationAction),
+                authenticationAction.pendingIntent)
+        }
+        /**
+         * Returns an instance of [AuthenticationAction] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): AuthenticationAction? {
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+                    return try {
+                        AuthenticationAction(it.action)
+                    } catch (e: Exception) {
+                        Log.i(TAG, "fromSlice failed with: " + e.message)
+                        null
+                    }
+                }
+            }
+            return null
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialProviderRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialProviderRequest.kt
new file mode 100644
index 0000000..d447f58
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialProviderRequest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import android.util.ArraySet
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * Base request class for registering a credential.
+ *
+ * Providers will receive a subtype of this request with the call.
+ *
+ * @hide
+ */
+@RequiresApi(34)
+abstract class BeginCreateCredentialProviderRequest internal constructor(
+    /** @hide */
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    open val type: String,
+    val callingAppInfo: CallingAppInfo
+    ) {
+    companion object {
+        internal fun createFrom(
+            type: String,
+            data: Bundle,
+            // TODO("Change to framework ApplicationInfo")
+            packageName: String
+        ): BeginCreateCredentialProviderRequest {
+            return try {
+                when (type) {
+                    PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
+                        BeginCreatePasswordCredentialRequest.createFrom(
+                            data,
+                            CallingAppInfo(packageName, ArraySet())
+                        )
+                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
+                        BeginCreatePublicKeyCredentialRequest.createFrom(
+                            data,
+                            CallingAppInfo(packageName, ArraySet())
+                        )
+                    else -> throw FrameworkClassParsingException()
+                }
+            } catch (e: FrameworkClassParsingException) {
+                BeginCreateCustomCredentialRequest(
+                    type,
+                    data,
+                    CallingAppInfo(packageName, ArraySet())
+                )
+            }
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialProviderResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialProviderResponse.kt
new file mode 100644
index 0000000..1d1bf3d
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCredentialProviderResponse.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.credentials.provider
+
+import android.service.credentials.BeginCreateCredentialResponse
+import android.util.Log
+import androidx.annotation.RequiresApi
+
+/**
+ * The response to a call.
+ *
+ * @property createEntries the list of [CreateEntry] that is presented to the user at the time
+ * of credential creation. Each entry corresponds to an account or group where the user could
+ * register the created with.
+ * @throws IllegalArgumentException If [createEntries] is empty
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class BeginCreateCredentialProviderResponse constructor(
+    val createEntries: List<CreateEntry>,
+    val remoteEntry: RemoteEntry?,
+    val header: CharSequence?
+    ) {
+
+    init {
+        require(createEntries.isNotEmpty()) { "createEntries must not be empty" }
+    }
+
+    /** Builder for [BeginCreateCredentialProviderResponse]. */
+    class Builder {
+        // TODO("Add header and remote entry")
+        private var createEntries: MutableList<CreateEntry> = mutableListOf()
+        private var header: CharSequence? = null
+        private var remoteEntry: RemoteEntry? = null
+
+        /** Adds a [CreateEntry] to be displayed on the selector */
+        fun addCreateEntry(createEntry: CreateEntry): Builder {
+            createEntries.add(createEntry)
+            return this
+        }
+
+        /** Sets a list of [CreateEntry] to be displayed on the selector */
+        fun setCreateEntries(createEntries: List<CreateEntry>): Builder {
+            this.createEntries = createEntries.toMutableList()
+            return this
+        }
+
+        /** Sets a header to be displayed on the selector */
+        fun setHeader(header: CharSequence?): Builder {
+            this.header = header
+            return this
+        }
+
+        /** Sets a [RemoteEntry] that denotes the flow can be completed on a remote device */
+        fun setRemoteEntry(remoteEntry: RemoteEntry?): Builder {
+            this.remoteEntry = remoteEntry
+            return this
+        }
+
+        /**
+         * Builds an instance of [BeginCreateCredentialProviderResponse]
+         *
+         * @throws IllegalArgumentException If [createEntries] is empty
+         */
+        fun build(): BeginCreateCredentialProviderResponse {
+            return BeginCreateCredentialProviderResponse(createEntries, remoteEntry, header)
+        }
+    }
+
+    companion object {
+        private const val TAG = "CreateResponse"
+        @JvmStatic
+        internal fun toFrameworkClass(response: BeginCreateCredentialProviderResponse):
+            BeginCreateCredentialResponse {
+            val builder = BeginCreateCredentialResponse.Builder()
+            response.createEntries.forEach {
+                try {
+                    builder.addCreateEntry(CreateEntry.toFrameworkClass(it))
+                } catch (e: Exception) {
+                    Log.i(TAG, "Issue while creating framework class: " + e.message)
+                }
+            }
+            builder.setRemoteCreateEntry(response.remoteEntry?.let {
+                RemoteEntry.toFrameworkCreateEntryClass(
+                    it
+                )
+            })
+            return builder.build()
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCustomCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCustomCredentialRequest.kt
new file mode 100644
index 0000000..856d79a
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreateCustomCredentialRequest.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreatePasswordRequest
+
+/**
+ * Request to begin saving a custom credential, received by the provider with a
+ * CredentialProviderBaseService.onBeginCreateCredentialRequest call.
+ *
+ * This request will not contain all parameters needed to store the credential. Provider must
+ * use the initial parameters to determine if the password can be stored, and return
+ * a list of [CreateEntry], denoting the accounts/groups where the password can be stored.
+ * When user selects one of the returned [CreateEntry], the corresponding [PendingIntent] set on
+ * the [CreateEntry] will be fired. The [Intent] invoked through the [PendingIntent] will contain the
+ * complete [CreatePasswordRequest]. This request will contain all required parameters to
+ * actually store the password.
+ *
+ * @property type the type of this custom credential type
+ * @property data the request parameters for the custom credential option
+ * @throws IllegalArgumentException If [type] is empty
+ * @throws NullPointerException If [type] or [data] is null
+ *
+ * @see BeginCreateCredentialProviderRequest
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class BeginCreateCustomCredentialRequest internal constructor(
+    override val type: String,
+    val data: Bundle,
+    callingAppInfo: CallingAppInfo
+) : BeginCreateCredentialProviderRequest(
+    type,
+    callingAppInfo) {
+    init {
+        require(type.isNotEmpty()) { "type must not be empty" }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt
new file mode 100644
index 0000000..4d9f070
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePasswordCredentialRequest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePasswordRequest.Companion.BUNDLE_KEY_ID
+import androidx.credentials.PasswordCredential
+
+/**
+ * Request to begin saving a password credential, received by the provider with a
+ * CredentialProviderBaseService.onBeginCreateCredentialRequest call.
+ *
+ * This request will not contain all parameters needed to store the password. Provider must
+ * use the initial parameters to determine if the password can be stored, and return
+ * a list of [CreateEntry], denoting the accounts/groups where the password can be stored.
+ * When user selects one of the returned [CreateEntry], the corresponding [PendingIntent] set on
+ * the [CreateEntry] will be fired. The [Intent] invoked through the [PendingIntent] will contain the
+ * complete [CreatePasswordRequest]. This request will contain all required parameters to
+ * actually store the password.
+ *
+ * @property id the id of the password to be stored
+ * @property callingAppInfo the information of the calling app for which the password needs to
+ * be stored
+ * @throws NullPointerException If [id] is null
+ * @throws IllegalArgumentException If [id] is empty
+ *
+ * @see BeginCreateCredentialProviderRequest
+ *
+ * @hide
+ */
+// TODO ("Add custom class similar to developer side")
+@RequiresApi(34)
+class BeginCreatePasswordCredentialRequest internal constructor(
+    val id: String,
+    callingAppInfo: CallingAppInfo
+) : BeginCreateCredentialProviderRequest(
+    PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+    callingAppInfo) {
+
+    init {
+        require(id.isNotEmpty()) { "id must not be empty" }
+    }
+
+    companion object {
+        @JvmStatic
+        internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo):
+            BeginCreatePasswordCredentialRequest {
+            return BeginCreatePasswordCredentialRequest(
+                data.getString(BUNDLE_KEY_ID)!!,
+                callingAppInfo
+            )
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt
new file mode 100644
index 0000000..fc8e761
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginCreatePublicKeyCredentialRequest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_REQUEST_JSON
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * Request to begin registering a public key credential, received by the provider with a
+ *
+ * This request will not contain all parameters needed to create the public key. Provider must
+ * use the initial parameters to determine if the public key can be registered, and return
+ * a list of [CreateEntry], denoting the accounts/groups where the public key can be registered.
+ * When user selects one of the returned [CreateEntry], the corresponding [PendingIntent] set on
+ * the [CreateEntry] will be fired. The [Intent] invoked through the [PendingIntent] will contain
+ * the complete [CreatePublicKeyCredentialRequest]. This request will contain all required
+ * parameters to actually register a public key.
+ *
+ * @property json the request json to be used for registering the public key credential
+ * @property callingAppInfo the information of the calling app for which the passwords needs to
+ * be stored
+ *
+ * @see BeginCreateCredentialProviderRequest
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class BeginCreatePublicKeyCredentialRequest internal constructor(
+    val json: String,
+    callingAppInfo: CallingAppInfo
+) : BeginCreateCredentialProviderRequest(
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+    callingAppInfo) {
+
+    init {
+        require(json.isNotEmpty()) { "json must not be empty" }
+    }
+
+    /** @hide */
+    companion object {
+        @JvmStatic
+        internal fun createFrom(data: Bundle, callingAppInfo: CallingAppInfo):
+            BeginCreatePublicKeyCredentialRequest {
+            try {
+                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+                return BeginCreatePublicKeyCredentialRequest(requestJson!!, callingAppInfo)
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt
new file mode 100644
index 0000000..5737f0f
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialOption.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.credentials.PasswordCredential
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.GetPublicKeyCredentialOptionPrivileged
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * Base class for getting a specific type of credentials, to be used in the query phase of a
+ * get flow.
+ *
+ * [BeginGetCredentialsProviderRequest] will be composed of a list of [BeginGetCredentialOption]
+ * subclasses to indicate the specific credential types and configurations that your app accepts.
+ *
+ * @hide
+ */
+abstract class BeginGetCredentialOption internal constructor(
+    /** @hide */
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    open val type: String,
+) {
+    /** @hide */
+    companion object {
+        /** @hide */
+        @JvmStatic
+        fun createFrom(
+            type: String,
+            candidateQueryData: Bundle
+        ): BeginGetCredentialOption {
+            return try {
+                when (type) {
+                    PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
+                        BeginGetPasswordOption.createFrom(candidateQueryData)
+                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
+                        when (candidateQueryData.getString(
+                            PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
+                            GetPublicKeyCredentialOption
+                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
+                                BeginGetPublicKeyCredentialOption.createFrom(candidateQueryData)
+                            GetPublicKeyCredentialOptionPrivileged
+                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
+                            -> BeginGetPublicKeyCredentialOptionPrivileged.createFrom(
+                                    candidateQueryData)
+                            else -> throw FrameworkClassParsingException()
+                        }
+                    else -> throw FrameworkClassParsingException()
+                }
+            } catch (e: FrameworkClassParsingException) {
+                // BeginGetCustomCredentialOption gets the requestData as is
+                BeginGetCustomCredentialOption(type, candidateQueryData)
+            }
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialsProviderRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialsProviderRequest.kt
new file mode 100644
index 0000000..8c9841d
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialsProviderRequest.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.credentials.provider
+
+/**
+ * Request for [CredentialProviderBaseService.onBeginGetCredentialsRequest] to
+ * credential providers.
+ *
+ * @property beginGetCredentialOptions a list of [BeginGetCredentialOption] where each option
+ * contains per credential type request parameters
+ * @property callingAppInfo information pertaining the calling application
+ * @throws IllegalArgumentException If [beginGetCredentialOptions] is empty
+ *
+ * @hide
+ */
+class BeginGetCredentialsProviderRequest internal constructor(
+    val beginGetCredentialOptions: List<BeginGetCredentialOption>,
+    val callingAppInfo: CallingAppInfo
+    ) {
+
+    init {
+        require(beginGetCredentialOptions.isNotEmpty()) {
+            "beginGetCredentialOptions must not be empty" }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialsProviderResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialsProviderResponse.kt
new file mode 100644
index 0000000..3f34190
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCredentialsProviderResponse.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.service.credentials.BeginGetCredentialsResponse
+import androidx.annotation.RequiresApi
+import androidx.credentials.internal.FrameworkClassParsingException
+import androidx.credentials.provider.BeginGetCredentialsProviderResponse.Companion.createWithAuthentication
+import androidx.credentials.provider.BeginGetCredentialsProviderResponse.Companion.createWithCredentialsResponseContent
+
+/**
+ * Response for [BeginGetCredentialsProviderRequest] from a credential provider.
+ *
+ * If the provider is locked and cannot return any credentials, the [createWithAuthentication]
+ * method should be used to create the response with an [AuthenticationAction]. When the user
+ * selects this [AuthenticationAction], the corresponding [PendingIntent] will be fired that can
+ * bring up the provider's unlock activity.
+ *
+ * If the provider is not locked and can return credential, the
+ * [createWithCredentialsResponseContent] method should be used to create the response with
+ * a list of [CredentialEntry], and a list of [Action].
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class BeginGetCredentialsProviderResponse internal constructor(
+    val credentialsResponseContent: CredentialsResponseContent?,
+    val authenticationAction: AuthenticationAction?
+    ) {
+
+    init {
+        require(!(credentialsResponseContent == null && authenticationAction == null)) {
+            "Both authenticationAction and credentialsDisplayProviderContent must not be null"
+        }
+    }
+    companion object {
+        @JvmStatic
+        fun createWithAuthentication(
+            authenticationAction: AuthenticationAction
+        ): BeginGetCredentialsProviderResponse {
+            return BeginGetCredentialsProviderResponse(
+                /*credentialsResponseContent=*/null, authenticationAction)
+        }
+
+        @JvmStatic
+        fun createWithCredentialsResponseContent(
+            credentialsResponseContent: CredentialsResponseContent
+        ): BeginGetCredentialsProviderResponse {
+            return BeginGetCredentialsProviderResponse(
+                credentialsResponseContent, /*authenticationAction=*/null)
+        }
+
+        @JvmStatic
+        internal fun toFrameworkClass(response: BeginGetCredentialsProviderResponse):
+            BeginGetCredentialsResponse {
+            // TODO("Return BeginGetCredentialsResponse when ready on the framework")
+            return if (response.credentialsResponseContent != null) {
+                BeginGetCredentialsResponse.createWithResponseContent(
+                    CredentialsResponseContent.toFrameworkClass(
+                        response.credentialsResponseContent)
+                )
+            } else if (response.authenticationAction != null) {
+                BeginGetCredentialsResponse.createWithAuthentication(
+                    AuthenticationAction.toFrameworkClass(response.authenticationAction)
+                )
+            } else {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt
new file mode 100644
index 0000000..27f3d01
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetCustomCredentialOption.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+
+/**
+ * Allows extending custom versions of BeginGetCredentialOptions for unique use cases.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass
+ * generated for custom use cases
+ * @property candidateQueryData the partial request data in the [Bundle] format that should be used
+ * to populate a list of [CredentialEntry] to be set on the
+ * [BeginGetCredentialsProviderResponse]
+ * @throws IllegalArgumentException If [type] is empty
+ *
+ * @hide
+ */
+open class BeginGetCustomCredentialOption internal constructor(
+    final override val type: String,
+    val candidateQueryData: Bundle,
+) : BeginGetCredentialOption(
+    type
+) {
+    init {
+        require(type.isNotEmpty()) { "type should not be empty" }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt
new file mode 100644
index 0000000..3c805c0
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPasswordOption.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.credentials.PasswordCredential
+
+/**
+ * A request to begin the flow of retrieving the user's saved application password from their
+ * password provider.
+ *
+ * @hide
+ */
+class BeginGetPasswordOption : BeginGetCredentialOption(
+    type = PasswordCredential.TYPE_PASSWORD_CREDENTIAL
+) {
+    /** @hide */
+    @Suppress("UNUSED_PARAMETER")
+    companion object {
+        @JvmStatic
+        internal fun createFrom(data: Bundle): BeginGetPasswordOption {
+            return BeginGetPasswordOption()
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt
new file mode 100644
index 0000000..ce243c3
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOption.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * A request to begin the flow of getting passkeys from the user's public key credential provider.
+ *
+ * @property requestJson the privileged request in JSON format in the standard webauthn web json
+ * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson)
+ * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
+ * true by default, with hybrid credentials defined
+ * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @throws NullPointerException If [requestJson] is null
+ * @throws IllegalArgumentException If [requestJson] is empty
+ *
+ * @hide
+ */
+class BeginGetPublicKeyCredentialOption @JvmOverloads internal constructor(
+    val requestJson: String,
+    @get:JvmName("allowHybrid")
+    val allowHybrid: Boolean = true,
+) : BeginGetCredentialOption(
+    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+) {
+    init {
+        require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
+    }
+
+    /** @hide */
+    companion object {
+        @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
+                                         // boolean value from being returned.
+        @JvmStatic
+        internal fun createFrom(data: Bundle): BeginGetPublicKeyCredentialOption {
+            try {
+                val requestJson = data.getString(
+                    GetPublicKeyCredentialOption.BUNDLE_KEY_REQUEST_JSON)
+                val allowHybrid = data.get(
+                    GetPublicKeyCredentialOption
+                        .BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                return BeginGetPublicKeyCredentialOption(requestJson!!, (allowHybrid!!) as Boolean)
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionPrivileged.kt
new file mode 100644
index 0000000..5ceeb06
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/BeginGetPublicKeyCredentialOptionPrivileged.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.credentials.provider
+
+import android.os.Bundle
+import androidx.credentials.GetPublicKeyCredentialOptionPrivileged
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.internal.FrameworkClassParsingException
+
+/**
+ * A privileged request to get passkeys from the user's public key credential provider. The caller
+ * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
+ * can use this. These permissions will be introduced in an upcoming release.
+ * TODO("Add specific permission info/annotation")
+ *
+ * @property requestJson the privileged request in JSON format in the standard webauthn web json
+ * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
+ * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
+ * true by default, with hybrid credentials defined
+ * [here](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid)
+ * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
+ * where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
+ * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
+ * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
+ * is null
+ * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is empty
+ *
+ * @hide
+ */
+class BeginGetPublicKeyCredentialOptionPrivileged @JvmOverloads internal constructor(
+    val requestJson: String,
+    val relyingParty: String,
+    val clientDataHash: String,
+    @get:JvmName("allowHybrid")
+    val allowHybrid: Boolean = true
+) : BeginGetCredentialOption(
+    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+) {
+
+    init {
+        require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
+        require(relyingParty.isNotEmpty()) { "rp must not be empty" }
+        require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
+    }
+
+    /** @hide */
+    companion object {
+        @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
+                                         // boolean value from being returned.
+        @JvmStatic
+        internal fun createFrom(data: Bundle): BeginGetPublicKeyCredentialOptionPrivileged {
+            try {
+                val requestJson = data.getString(
+                    GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_REQUEST_JSON)
+                val rp = data.getString(
+                    GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RELYING_PARTY)
+                val clientDataHash = data.getString(
+                    GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH)
+                val allowHybrid = data.get(
+                    GetPublicKeyCredentialOptionPrivileged
+                        .BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
+                return BeginGetPublicKeyCredentialOptionPrivileged(
+                    requestJson!!,
+                    rp!!,
+                    clientDataHash!!,
+                    (allowHybrid!!) as Boolean,
+                )
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
new file mode 100644
index 0000000..4ddeb93
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CallingAppInfo.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.credentials.provider
+
+/**
+ * Information about the calling app. This is a read-only data class that
+ * providers should use to retrieve information about the calling app.
+ *
+ * @property appPackage the package name of the calling app
+ * @property appSignature the app signature of the calling app
+ *
+ * @hide
+ */
+class CallingAppInfo internal constructor(
+    val appPackage: String,
+    // TODO("Check for non empty when framework change available")
+    val appSignature: Set<android.content.pm.Signature>
+    ) {
+
+    init {
+        require(appPackage.isNotEmpty()) {
+            "appPackage must not be empty"
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateCredentialProviderRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateCredentialProviderRequest.kt
new file mode 100644
index 0000000..8f099b5
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateCredentialProviderRequest.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.credentials.provider
+
+import androidx.annotation.RequiresApi
+import androidx.credentials.CreateCredentialRequest
+
+/**
+ * Request class for registering a credential.
+ *
+ * This request contains the actual request coming from the calling app,
+ * and the application information associated with the calling app
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class CreateCredentialProviderRequest internal constructor(
+    val callingRequest: CreateCredentialRequest,
+    val callingAppInfo: CallingAppInfo
+)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
new file mode 100644
index 0000000..d1e6b87
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CreateEntry.kt
@@ -0,0 +1,254 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.CredentialManager
+import androidx.credentials.provider.Action.Companion.toSlice
+import java.util.Collections
+
+/**
+ * An entry to be shown on the selector during a create flow initiated when an app calls
+ * [CredentialManager.executeCreateCredential]
+ *
+ * A [CreateEntry] points to a location such as an account, or a group where the credential can be
+ * 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 registered
+ * @property pendingIntent the [PendingIntent] to be fired when this
+ * [CreateEntry] is selected
+ * @property lastUsedTimeMillis the last used time of the account/group underlying this entry
+ * @property credentialCountInformationList a list of count information per credential type
+ * @throws IllegalArgumentException If [accountName] is empty
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class CreateEntry internal constructor(
+    val accountName: CharSequence,
+    val pendingIntent: PendingIntent,
+    val icon: Icon?,
+    val lastUsedTimeMillis: Long,
+    val credentialCountInformationList: List<CredentialCountInformation>
+    ) {
+
+    init {
+        require(accountName.isNotEmpty()) { "accountName must not be empty" }
+    }
+
+    /**
+     * A builder for [CreateEntry]
+     *
+     * @property accountName the name of the account where the credential will be registered
+     * @property pendingIntent the [PendingIntent] that will be fired when the user selects
+     * this entry
+     *
+     * @hide
+     */
+    class Builder constructor(
+        private val accountName: CharSequence,
+        private val pendingIntent: PendingIntent
+        ) {
+
+        private var credentialCountInformationList: MutableList<CredentialCountInformation> =
+            mutableListOf()
+        private var icon: Icon? = null
+        private var lastUsedTimeMillis: Long = 0
+
+        /** Adds a [CredentialCountInformation] denoting a given credential
+         * type and the count of credentials that the provider has stored for that
+         * credential type.
+         *
+         * This information will be displayed on the [CreateEntry] to help the user
+         * make a choice.
+         */
+        @Suppress("MissingGetterMatchingBuilder")
+        fun addCredentialCountInformation(info: CredentialCountInformation): Builder {
+            credentialCountInformationList.add(info)
+            return this
+        }
+
+        /** Sets a list of [CredentialCountInformation]. Each item in the list denotes a given
+         * credential type and the count of credentials that the provider has stored of that
+         * credential type.
+         *
+         * This information will be displayed on the [CreateEntry] to help the user
+         * make a choice.
+         */
+        fun setCredentialCountInformationList(infoList: List<CredentialCountInformation>): Builder {
+            credentialCountInformationList = infoList as MutableList<CredentialCountInformation>
+            return this
+        }
+
+        /** Sets an icon to be displayed with the entry on the UI */
+        fun setIcon(icon: Icon?): Builder {
+            this.icon = icon
+            return this
+        }
+
+        /** Sets the last time this account was used */
+        fun setLastUsedTimeMillis(lastUsedTimeMillis: Long): Builder {
+            this.lastUsedTimeMillis = lastUsedTimeMillis
+            return this
+        }
+
+        /**
+         * Builds an instance of [CreateEntry]
+         *
+         * @throws IllegalArgumentException If [accountName] is empty
+         */
+        fun build(): CreateEntry {
+            return CreateEntry(accountName, pendingIntent, icon, lastUsedTimeMillis,
+                credentialCountInformationList)
+        }
+    }
+
+    companion object {
+        private const val TAG = "CreateEntry"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_ACCOUNT_NAME =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_ICON =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
+
+        @JvmStatic
+        fun toSlice(createEntry: CreateEntry): Slice {
+            // TODO("Use the right type and revision")
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
+            sliceBuilder.addText(createEntry.accountName, /*subType=*/null,
+                listOf(SLICE_HINT_ACCOUNT_NAME))
+                .addLong(createEntry.lastUsedTimeMillis, /*subType=*/null, listOf(
+                    SLICE_HINT_LAST_USED_TIME_MILLIS))
+            if (createEntry.icon != null) {
+                sliceBuilder.addIcon(createEntry.icon, /*subType=*/null,
+                    listOf(SLICE_HINT_ICON))
+            }
+
+                val credentialCountBundle = convertCredentialCountInfoToBundle(
+                    createEntry.credentialCountInformationList)
+                if (credentialCountBundle != null) {
+                    sliceBuilder.addBundle(convertCredentialCountInfoToBundle(
+                        createEntry.credentialCountInformationList), null, listOf(
+                        SLICE_HINT_CREDENTIAL_COUNT_INFORMATION))
+                }
+            sliceBuilder.addAction(createEntry.pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null)
+            return sliceBuilder.build()
+        }
+
+        /**
+         * Returns an instance of [CreateEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): CreateEntry? {
+            // TODO("Put the right spec and version value")
+            var accountName: CharSequence = ""
+            var icon: Icon? = null
+            var pendingIntent: PendingIntent? = null
+            var credentialCountInfo: List<CredentialCountInformation> = listOf()
+            var lastUsedTimeMillis: Long = 0
+
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {
+                    accountName = it.text
+                } else if (it.hasHint(SLICE_HINT_ICON)) {
+                    icon = it.icon
+                } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+                    pendingIntent = it.action
+                } else if (it.hasHint(SLICE_HINT_CREDENTIAL_COUNT_INFORMATION)) {
+                    credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)
+                } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
+                    lastUsedTimeMillis = it.long
+                }
+            }
+
+            return try {
+                CreateEntry(accountName, pendingIntent!!, icon,
+                    lastUsedTimeMillis, credentialCountInfo)
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+
+        @JvmStatic
+        internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):
+            List<CredentialCountInformation> {
+            val credentialCountList = ArrayList<CredentialCountInformation>()
+            if (bundle == null) {
+                return credentialCountList
+            }
+            bundle.keySet().forEach {
+                try {
+                    credentialCountList.add(
+                        CredentialCountInformation(it, bundle.getInt(it)))
+                } catch (e: Exception) {
+                    Log.i(TAG, "Issue unpacking credential count info bundle: " + e.message)
+                }
+            }
+            return credentialCountList
+        }
+
+        @JvmStatic
+        internal fun convertCredentialCountInfoToBundle(
+            credentialCountInformationList: List<CredentialCountInformation>
+        ): Bundle? {
+            if (credentialCountInformationList.isEmpty()) {
+                return null
+            }
+            val bundle = Bundle()
+            credentialCountInformationList.forEach {
+                bundle.putInt(it.type, it.count)
+            }
+            return bundle
+        }
+
+        @JvmStatic
+        internal fun toFrameworkClass(createEntry: CreateEntry):
+            android.service.credentials.CreateEntry {
+            return android.service.credentials.CreateEntry(
+                toSlice(createEntry),
+                createEntry.pendingIntent)
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialCountInformation.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialCountInformation.kt
new file mode 100644
index 0000000..3e2f35d
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialCountInformation.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.credentials.provider
+
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+
+/** A credential count information class that takes in
+ * a credential type, and a count of credential stored
+ * for that type.
+ *
+ * Providers add this information to [CreateEntry] and it is
+ * displayed with that entry on the selector. This information
+ * helps users select this entry over others.
+ *
+ * @property type the type of the credential
+ * @property count the number of credentials stored for this credential type
+ *
+ * @hide
+ */
+class CredentialCountInformation constructor(
+    val type: String,
+    val count: Int
+    ) {
+    companion object {
+        @JvmStatic
+        fun createPasswordCountInformation(count: Int): CredentialCountInformation {
+            return CredentialCountInformation(PasswordCredential.TYPE_PASSWORD_CREDENTIAL, count)
+        }
+
+        @JvmStatic
+        fun createPublicKeyCountInformation(count: Int): CredentialCountInformation {
+            return CredentialCountInformation(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, count)
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
new file mode 100644
index 0000000..c1df1dd
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialEntry.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.provider.Action.Companion.toSlice
+import androidx.credentials.provider.CreateEntry.Companion.toSlice
+import java.util.Collections
+
+/**
+ * Base class for a credential entry that is displayed on the account selector UI.
+ * Each entry corresponds to an account that can provide a credential
+ *
+ * @property type the type of the credential
+ * @property username the username of the account holding the credential
+ * @property displayName the displayName of the account holding the credential
+ * @property pendingIntent the [PendingIntent] to be invoked when the user selects this entry
+ * only one of the selector
+ * @property lastUsedTimeMillis the last used time of this entry
+ * @property icon the icon to be displayed with this entry on the selector
+ *
+ * @throws IllegalArgumentException If [type] or [username] is empty, or [pendingIntent] is null
+ * are non null
+ *
+ * @hide
+ */
+@RequiresApi(34)
+open class CredentialEntry constructor(
+    // TODO("Add credential type display name for both CredentialEntry & CreateEntry")
+    val type: String,
+    val typeDisplayName: CharSequence,
+    val username: CharSequence,
+    val displayName: CharSequence?,
+    val pendingIntent: PendingIntent,
+    // TODO("Consider using Instant or other strongly typed time data type")
+    val lastUsedTimeMillis: Long,
+    val icon: Icon?,
+    var autoSelectAllowed: Boolean
+    ) {
+    init {
+        require(type.isNotEmpty()) { "type must not be empty" }
+        require(username.isNotEmpty()) { "type must not be empty" }
+    }
+
+    companion object {
+        private const val TAG = "CredentialEntry"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_USERNAME =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_DISPLAYNAME =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_ICON =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_AUTO_ALLOWED =
+            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val AUTO_SELECT_TRUE_STRING = "true"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val AUTO_SELECT_FALSE_STRING = "false"
+
+        @JvmStatic
+        internal fun toSlice(credentialEntry: CredentialEntry): Slice {
+            // TODO("Put the right revision value")
+            val autoSelectAllowed = if (credentialEntry.autoSelectAllowed) {
+                AUTO_SELECT_TRUE_STRING
+            } else {
+                AUTO_SELECT_FALSE_STRING
+            }
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(
+                credentialEntry.type, 1))
+                .addText(credentialEntry.typeDisplayName, /*subType=*/null,
+                    listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
+                .addText(credentialEntry.username, /*subType=*/null,
+                    listOf(SLICE_HINT_USERNAME))
+                .addText(credentialEntry.displayName, /*subType=*/null,
+                    listOf(SLICE_HINT_DISPLAYNAME))
+                .addLong(credentialEntry.lastUsedTimeMillis, /*subType=*/null,
+                    listOf(SLICE_HINT_LAST_USED_TIME_MILLIS))
+                .addText(autoSelectAllowed, /*subType=*/null,
+                    listOf(SLICE_HINT_AUTO_ALLOWED))
+            if (credentialEntry.icon != null) {
+                sliceBuilder.addIcon(credentialEntry.icon, /*subType=*/null,
+                    listOf(SLICE_HINT_ICON))
+            }
+            sliceBuilder.addAction(credentialEntry.pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null)
+            return sliceBuilder.build()
+        }
+
+        /**
+         * Returns an instance of [CredentialEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): CredentialEntry? {
+            var typeDisplayName: CharSequence? = null
+            var username: CharSequence? = null
+            var displayName: CharSequence? = null
+            var icon: Icon? = null
+            var pendingIntent: PendingIntent? = null
+            var lastUsedTimeMillis: Long = 0
+            var autoSelectAllowed = false
+
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
+                    typeDisplayName = it.text
+                } else if (it.hasHint(SLICE_HINT_USERNAME)) {
+                    username = it.text
+                } else if (it.hasHint(SLICE_HINT_DISPLAYNAME)) {
+                    displayName = it.text
+                } else if (it.hasHint(SLICE_HINT_ICON)) {
+                    icon = it.icon
+                } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+                    pendingIntent = it.action
+                } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
+                    lastUsedTimeMillis = it.long
+                } else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
+                    val autoSelectValue = it.text
+                    if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
+                        autoSelectAllowed = true
+                    }
+                }
+            }
+
+            return try {
+                CredentialEntry(slice.spec!!.type, typeDisplayName!!, username!!,
+                    displayName, pendingIntent!!,
+                    lastUsedTimeMillis, icon, autoSelectAllowed)
+            } catch (e: Exception) {
+                Log.i(TAG, "fromSlice failed with: " + e.message)
+                null
+            }
+        }
+
+        internal fun toFrameworkClass(credentialEntry: CredentialEntry):
+            android.service.credentials.CredentialEntry {
+            return android.service.credentials.CredentialEntry.Builder(
+                credentialEntry.type, toSlice(credentialEntry),
+                credentialEntry.pendingIntent
+            ).build()
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
new file mode 100644
index 0000000..022055c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderBaseService.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.credentials.provider
+
+import android.os.CancellationSignal
+import android.os.OutcomeReceiver
+import android.service.credentials.BeginCreateCredentialResponse
+import android.service.credentials.CredentialProviderService
+import android.service.credentials.BeginGetCredentialsRequest
+import android.service.credentials.BeginGetCredentialsResponse
+import android.util.ArraySet
+import android.util.Log
+import androidx.annotation.RequiresApi
+
+/**
+ * Credential Provider base service to be extended by provider services.
+ *
+ * This class extends from the framework [CredentialProviderService], and is
+ * called by the framework on credential get and create requests. The framework
+ * requests are converted to structured jetpack requests, and sent to
+ * provider services that extend from this service.
+ *
+ * @hide
+ */
+@RequiresApi(34)
+abstract class CredentialProviderBaseService : CredentialProviderService() {
+    final override fun onBeginGetCredentials(
+        request: BeginGetCredentialsRequest,
+        cancellationSignal: CancellationSignal,
+        callback: OutcomeReceiver<BeginGetCredentialsResponse,
+            android.service.credentials.CredentialProviderException>
+    ) {
+        val beginGetCredentialOptions: MutableList<BeginGetCredentialOption> = mutableListOf()
+        request.beginGetCredentialOptions.forEach {
+            beginGetCredentialOptions.add(
+                BeginGetCredentialOption
+                    .createFrom(
+                        it.type,
+                        it.candidateQueryData))
+        }
+        val structuredRequest =
+            BeginGetCredentialsProviderRequest(
+                beginGetCredentialOptions,
+                CallingAppInfo(request.callingPackage,
+                    ArraySet()
+                ))
+        val outcome = object : OutcomeReceiver<BeginGetCredentialsProviderResponse,
+            CredentialProviderException> {
+            override fun onResult(response: BeginGetCredentialsProviderResponse?) {
+                Log.i(TAG, "onGetCredentials response returned from provider " +
+                    "to jetpack library")
+                callback.onResult(response?.let {
+                    BeginGetCredentialsProviderResponse.toFrameworkClass(it) })
+            }
+
+            override fun onError(error: CredentialProviderException) {
+                super.onError(error)
+                Log.i(TAG, "onGetCredentials error returned from provider " +
+                    "to jetpack library")
+                // TODO("Change error code to provider error when ready on framework")
+                error.message?.let {
+                    callback.onError(android.service.credentials.CredentialProviderException(
+                        android.service.credentials.CredentialProviderException.ERROR_UNKNOWN, it)
+                    )
+                    return
+                }
+                callback.onError(android.service.credentials.CredentialProviderException(
+                    android.service.credentials.CredentialProviderException.ERROR_UNKNOWN))
+            }
+        }
+        onBeginGetCredentialsRequest(structuredRequest, cancellationSignal, outcome)
+    }
+
+    final override fun onBeginCreateCredential(
+        request: android.service.credentials.BeginCreateCredentialRequest,
+        cancellationSignal: CancellationSignal,
+        callback: OutcomeReceiver<BeginCreateCredentialResponse,
+            android.service.credentials.CredentialProviderException>
+    ) {
+        val outcome = object : OutcomeReceiver<
+            BeginCreateCredentialProviderResponse, CredentialProviderException> {
+            override fun onResult(response: BeginCreateCredentialProviderResponse?) {
+                Log.i(
+                    TAG, "onCreateCredential result returned from provider to jetpack " +
+                        "library with credential entries size: " + response?.createEntries?.size)
+                callback.onResult(response?.let {
+                    BeginCreateCredentialProviderResponse.toFrameworkClass(it) })
+            }
+            override fun onError(error: CredentialProviderException) {
+                Log.i(
+                    TAG, "onCreateCredential result returned from provider to jetpack")
+                super.onError(error)
+                // TODO("Change error code to provider error when ready on framework")
+                error.message?.let {
+                    callback.onError(android.service.credentials.CredentialProviderException(
+                        android.service.credentials.CredentialProviderException.ERROR_UNKNOWN, it)
+                    )
+                    return
+                }
+                callback.onError(android.service.credentials.CredentialProviderException(
+                    android.service.credentials.CredentialProviderException.ERROR_UNKNOWN))
+            }
+        }
+        onBeginCreateCredentialRequest(
+            BeginCreateCredentialProviderRequest.createFrom(
+                request.type,
+                request.data,
+                request.callingPackage),
+            cancellationSignal, outcome)
+    }
+
+    /**
+     * Called by the Credential Manager Jetpack library to get credentials stored with a provider
+     * service. Provider services must extend this in order to handle a
+     * [GetCredentialProviderRequest] request.
+     *
+     * Provider service must call one of the [callback] methods to notify the result of the
+     * request.
+     *
+     * @param [request] the [GetCredentialProviderRequest] to handle
+     * See [BeginGetCredentialsProviderResponse] for the response to be returned
+     * @param cancellationSignal signal for observing cancellation requests. The system will
+     * use this to notify you that the result is no longer needed and you should stop
+     * handling it in order to save your resources
+     * @param callback the callback object to be used to notify the response or error
+     *
+     * @hide
+     */
+    abstract fun onBeginGetCredentialsRequest(
+        request: BeginGetCredentialsProviderRequest,
+        cancellationSignal: CancellationSignal,
+        callback: OutcomeReceiver<BeginGetCredentialsProviderResponse, CredentialProviderException>
+    )
+
+    /**
+     * Called by the Credential Manager Jetpack library to begin a credential registration flow
+     * with a credential provider service. Provider services must extend this in order to handle a
+     * [BeginCreateCredentialProviderRequest] request.
+     *
+     * Provider service must call one of the [callback] methods to notify the result of the
+     * request.
+     *
+     * @param [request] the [BeginCreateCredentialProviderRequest] to handle
+     * See [BeginCreateCredentialProviderResponse] for the response to be returned
+     * @param cancellationSignal signal for observing cancellation requests. The system will
+     * use this to notify you that the result is no longer needed and you should stop
+     * handling it in order to save your resources
+     * @param callback the callback object to be used to notify the response or error
+     *
+     * @hide
+     */
+    abstract fun onBeginCreateCredentialRequest(
+        request: BeginCreateCredentialProviderRequest,
+        cancellationSignal: CancellationSignal,
+        callback: OutcomeReceiver<BeginCreateCredentialProviderResponse,
+            CredentialProviderException>
+    )
+
+    companion object {
+        private const val TAG = "BaseService"
+    }
+}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderException.kt
similarity index 60%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderException.kt
index 7053e2d..6d6b49e6 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialProviderException.kt
@@ -14,6 +14,15 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.credentials.provider
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+/**
+ * An exception to be thrown if there is a failure while executing either
+ * [CredentialProviderBaseService.onBeginCreateCredentialRequest] or
+ * [CredentialProviderBaseService.onBeginCreateCredential]
+ *
+ * @hide
+ */
+class CredentialProviderException @JvmOverloads constructor(
+    errorMessage: CharSequence? = null
+) : Exception(errorMessage?.toString())
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialsResponseContent.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialsResponseContent.kt
new file mode 100644
index 0000000..c84373b
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/CredentialsResponseContent.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.util.Log
+import androidx.annotation.RequiresApi
+import java.util.Objects
+
+/**
+ * Content to be presented to the user on the account selector UI, including credential entries,
+ * and provider related actions.
+ *
+ * @property credentialEntries the list of [CredentialEntry] to be shown on the selector UI
+ * @property actions the list of [Action] entries to be shown on the selector UI
+ * @property remoteEntry an entry denoting that the flow can be completed on a remote device
+ * @throws IllegalStateException if both [credentialEntries] and [actions] are empty
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class CredentialsResponseContent internal constructor(
+    val credentialEntries: List<CredentialEntry>,
+    val actions: List<Action>,
+    val remoteEntry: RemoteEntry?
+    ) {
+
+    init {
+        check(!(credentialEntries.isEmpty() && actions.isEmpty())) {
+            ("credentialEntries and actions must not both be empty")
+        }
+    }
+
+    /**
+     * Builds an instance of [CredentialsResponseContent].
+     */
+    class Builder {
+        // TODO("Add remote entry")
+        private var credentialEntries: MutableList<CredentialEntry> = mutableListOf()
+        private var actions: MutableList<Action> = mutableListOf()
+        private var remoteEntry: RemoteEntry? = null
+
+        /**
+         * Add a [CredentialEntry] to the [CredentialsResponseContent], to be shown on the
+         * selector UI as an option that can return a credential on selection.
+         */
+        fun addCredentialEntry(credentialEntry: CredentialEntry): Builder {
+            credentialEntries.add(Objects.requireNonNull(credentialEntry))
+            return this
+        }
+
+        /**
+         * Set a list a [CredentialEntry] to the [CredentialsResponseContent], to be shown on the
+         * selector UI as options that can return a credential on selection.
+         */
+        fun setCredentialEntries(
+            credentialEntryList: List<CredentialEntry>
+        ): Builder {
+            this.credentialEntries = credentialEntryList.toMutableList()
+            return this
+        }
+
+        /**
+         * Add an [Action] to the [CredentialsResponseContent], to be shown on the selector
+         * UI. An [Action] can be used to offer non-credential actions to the user such as
+         * opening the app, managing credentials etc. When selected, the corresponding
+         * [PendingIntent] is fired, that can start a provider activity.
+         */
+        fun addAction(action: Action): Builder {
+            actions.add(Objects.requireNonNull(action, "action must not be null"))
+            return this
+        }
+
+        /**
+         * Sets a list of [Action] to the [CredentialsResponseContent], to be shown on the selector
+         * UI. An [Action] can be used to offer non-credential actions to the user such as
+         * opening the app, managing credentials etc. When selected, the corresponding
+         * [PendingIntent] is fired, that can start a provider activity.
+         */
+        fun setActions(actionList: List<Action>): Builder {
+            this.actions = actionList.toMutableList()
+            return this
+        }
+
+        /**
+         * Sets a a [RemoteEntry] denoting that this flow can be completed on a remote device
+         */
+        fun setRemoteEntry(remoteEntry: RemoteEntry?): Builder {
+            this.remoteEntry = remoteEntry
+            return this
+        }
+
+        /** Builds an instance of [CredentialsResponseContent]
+         *
+         * @throws IllegalStateException if both [credentialEntries] and [actions] are empty
+         */
+        fun build(): CredentialsResponseContent {
+            return CredentialsResponseContent(credentialEntries, actions, remoteEntry)
+        }
+    }
+
+    companion object {
+        private const val TAG = "ResponseContent"
+        @JvmStatic
+        internal fun toFrameworkClass(credentialsResponseContent: CredentialsResponseContent):
+            android.service.credentials.CredentialsResponseContent {
+            val builder = android.service.credentials.CredentialsResponseContent.Builder()
+
+            // Add all the credential entries
+            credentialsResponseContent.credentialEntries.forEach {
+                try {
+                    builder.addCredentialEntry(
+                        CredentialEntry.toFrameworkClass(it))
+                } catch (e: Exception) {
+                    Log.i(TAG, "Issue parsing a credentialEntry: " + e.message)
+                }
+            }
+
+            // Add all the actions
+            credentialsResponseContent.actions.forEach {
+                try {
+                    builder.addAction(Action.toFrameworkClass(it))
+                } catch (e: Exception) {
+                    Log.i(TAG, "Issue parsing an action: " + e.message)
+                }
+            }
+            builder.setRemoteCredentialEntry(credentialsResponseContent.remoteEntry?.let {
+                RemoteEntry.toFrameworkCredentialEntryClass(
+                    it
+                )
+            })
+            return builder.build()
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/GetCredentialProviderRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/GetCredentialProviderRequest.kt
new file mode 100644
index 0000000..84e1d7e
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/GetCredentialProviderRequest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.util.ArraySet
+import androidx.annotation.RequiresApi
+import androidx.credentials.GetCredentialOption
+import androidx.credentials.GetCredentialRequest
+
+/**
+ * Request received by the provider after the query phase of the get flow is complete and the
+ * user has made a selection from the list of [CredentialEntry] that was set on the
+ * [BeginGetCredentialsProviderResponse].
+ *
+ * This request will be added to the intent that starts the activity invoked by the [PendingIntent]
+ * set on the [CredentialEntry] that the user selected. The request can be extracted by using
+ * the PendingIntentHandler.
+ *
+ * @property callingAppRequest an instance of [GetCredentialRequest] that contains the credential
+ * type request parameters for the final credential request
+ * @property callingAppInfo information pertaining to the calling application
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class GetCredentialProviderRequest internal constructor(
+    val callingAppRequest: GetCredentialRequest,
+    val callingAppInfo: CallingAppInfo
+    ) {
+    companion object {
+        internal fun createFrom(request: android.service.credentials.GetCredentialRequest):
+        GetCredentialProviderRequest {
+            val options = ArrayList<GetCredentialOption>()
+            request.getCredentialOptions.forEach {
+                options.add(GetCredentialOption.createFrom(
+                    it.type, it.candidateQueryData,
+                    it.credentialRetrievalData, it.requireSystemProvider()))
+            }
+            return GetCredentialProviderRequest(
+                GetCredentialRequest.Builder()
+                    .setGetCredentialOptions(options)
+                    .setAutoSelectAllowed(false)
+                    .build(),
+                CallingAppInfo(
+                    request.callingPackage,
+                    ArraySet()
+                ))
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
new file mode 100644
index 0000000..47b05c1
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PasswordCredentialEntry.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.annotation.RequiresApi
+import androidx.credentials.PasswordCredential
+import androidx.credentials.R
+
+/**
+ * A password credential entry that is displayed on the account selector UI. This
+ * entry denotes that a credential of type [PasswordCredential.TYPE_PASSWORD_CREDENTIAL]
+ * is available for the user.
+ *
+ * @property username the username of the account holding the password credential
+ * @property displayName the displayName of the account holding the password credential
+ * @property pendingIntent the [PendingIntent] to be invoked when the user selects this entry
+ * @property lastUsedTimeMillis the last used time of this entry
+ * @property icon the icon to be displayed with this entry on the selector
+ *
+ * @throws IllegalArgumentException if [username] is empty, or [pendingIntent] is null
+ * are non null
+ *
+ * @see CredentialEntry
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class PasswordCredentialEntry internal constructor(
+    typeDisplayName: CharSequence,
+    username: CharSequence,
+    displayName: CharSequence?,
+    pendingIntent: PendingIntent,
+    lastUsedTimeMillis: Long,
+    icon: Icon,
+    autoSelectAllowed: Boolean
+) : CredentialEntry(PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+    typeDisplayName, username, displayName, pendingIntent, lastUsedTimeMillis,
+    icon, autoSelectAllowed
+) {
+
+    /**
+     * Builder for [PasswordCredentialEntry]
+     *
+     * @property displayName the displayname of the account holding the credential
+     * @property pendingIntent the [PendingIntent] to be invoked when the user selects this entry
+     * @property lastUsedTimeMillis the last used time of this entry
+     * @property icon the icon to be displayed with this entry on the selector
+     *
+     * @hide
+     */
+    class Builder {
+        // TODO("Add auto select")
+        private val context: Context
+        private val username: CharSequence
+        private var displayName: CharSequence? = null
+        private var pendingIntent: PendingIntent? = null
+        private var lastUsedTimeMillis: Long = 0
+        private var icon: Icon? = null
+        private var autoSelectAllowed = false
+
+        /**
+         * @property username the username of the account holding the credential
+         * @property pendingIntent the [PendingIntent] to be invoked when the entry is selected
+         *
+         * Providers should use this constructor when an additional activity is required
+         * before returning the final [PasswordCredential]
+         */
+        constructor(context: Context, username: CharSequence, pendingIntent: PendingIntent) {
+            this.context = context
+            this.username = username
+            this.pendingIntent = pendingIntent
+        }
+
+        /** Sets a displayname to be shown on the UI with this entry */
+        fun setDisplayName(displayName: CharSequence?): Builder {
+            this.displayName = displayName
+            return this
+        }
+
+        /** Sets the icon to be shown on the UI with this entry */
+        fun setIcon(icon: Icon?): Builder {
+            this.icon = icon
+            return this
+        }
+
+        /**
+         * Sets the last used time of this account
+         *
+         * This information will be used to sort the entries on the selector.
+         */
+        fun setLastUsedTimeMillis(lastUsedTimeMillis: Long): Builder {
+            this.lastUsedTimeMillis = lastUsedTimeMillis
+            return this
+        }
+
+        /** Builds an instance of [PasswordCredentialEntry] */
+        fun build(): PasswordCredentialEntry {
+            if (icon == null) {
+                icon = Icon.createWithResource(context, R.drawable.ic_password)
+            }
+            val typeDisplayName = context.getString(
+                R.string.android_credentials_TYPE_PASSWORD_CREDENTIAL)
+            return PasswordCredentialEntry(typeDisplayName,
+                username, displayName, pendingIntent!!,
+                lastUsedTimeMillis, icon!!, autoSelectAllowed)
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
new file mode 100644
index 0000000..688d576
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PendingIntentHandler.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.credentials.CreateCredentialResponse
+import android.service.credentials.CreateCredentialRequest
+import android.service.credentials.CredentialProviderService
+import android.util.ArraySet
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.credentials.GetCredentialResponse
+
+/**
+ * PendingIntentHandler to be used by credential providers to extract requests from
+ * [PendingIntent] invoked when a given [CreateEntry], or a [CredentialEntry]
+ * is selected by the user.
+ *
+ * This handler can also be used to set [android.credentials.CreateCredentialResponse] and
+ * [android.credentials.GetCredentialResponse] on the result of the activity
+ * invoked by the [PendingIntent]
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class PendingIntentHandler {
+    companion object {
+        private const val TAG = "PendingIntentHandler"
+
+        /**
+         * Extract the [CreateCredentialProviderRequest] from the provider's
+         * [PendingIntent] invoked by the Android system.
+         *
+         * @hide
+         */
+        @JvmStatic
+        fun getCreateCredentialRequest(intent: Intent): CreateCredentialProviderRequest? {
+            val frameworkReq: CreateCredentialRequest? =
+                intent.getParcelableExtra(
+                CredentialProviderService
+                    .EXTRA_CREATE_CREDENTIAL_REQUEST, CreateCredentialRequest::class.java
+                )
+            if (frameworkReq == null) {
+                Log.i(TAG, "Request not found in pendingIntent")
+                return frameworkReq
+            }
+            return CreateCredentialProviderRequest(
+                androidx.credentials.CreateCredentialRequest
+                    .createFrom(
+                        frameworkReq.type,
+                        frameworkReq.data,
+                        frameworkReq.data,
+                        requireSystemProvider = false),
+                CallingAppInfo(
+                    frameworkReq.callingPackage,
+                    ArraySet()
+                ))
+        }
+
+        /**
+         * Set the [CreateCredentialResponse] on the result of the
+         * activity invoked by the [PendingIntent] set on
+         * [CreateEntry]
+         *
+         * @hide
+         */
+        @JvmStatic
+        fun setCreateCredentialResponse(
+            intent: Intent,
+            response: androidx.credentials.CreateCredentialResponse
+        ) {
+            intent.putExtra(
+                CredentialProviderService.EXTRA_CREATE_CREDENTIAL_RESPONSE,
+                CreateCredentialResponse(response.data))
+        }
+
+        /**
+         * Extract the [GetCredentialProviderRequest] from the provider's
+         * [PendingIntent] invoked by the Android system.
+         *
+         * @hide
+         */
+        @JvmStatic
+        fun getGetCredentialsRequest(intent: Intent):
+            GetCredentialProviderRequest? {
+            val frameworkReq = intent.getParcelableExtra(
+                CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
+                android.service.credentials.GetCredentialRequest::class.java
+            )
+            if (frameworkReq == null) {
+                Log.i(TAG, "Get request from framework is null")
+                return null
+            }
+            return GetCredentialProviderRequest.createFrom(frameworkReq)
+        }
+
+        /**
+         * Set the [android.credentials.GetCredentialResponse] on the result of the
+         * activity invoked by the [PendingIntent] set on [CreateEntry]
+         *
+         * @hide
+         */
+        @JvmStatic
+        fun setGetCredentialResponse(
+            intent: Intent,
+            response: GetCredentialResponse
+        ) {
+            intent.putExtra(
+                CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+                android.credentials.GetCredentialResponse(
+                    android.credentials.Credential(response.credential.type,
+                        response.credential.data))
+            )
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
new file mode 100644
index 0000000..ee1ba97
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/PublicKeyCredentialEntry.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.credentials.provider
+
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.annotation.RequiresApi
+import androidx.credentials.PasswordCredential
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.R
+
+/**
+ * A public key credential entry that is displayed on the account selector UI.
+ * This entry denotes that a credential of type [PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL]
+ * is available for the user.
+ *
+ * @property username the username of the account holding the public key credential
+ * @property displayName the displayName of the account holding the public key credential
+ * @property pendingIntent the [PendingIntent] to be invoked when the user selects this entry
+ * @property lastUsedTimeMillis the last used time of this entry
+ * @property icon the icon to be displayed with this entry on the selector
+ *
+ * @throws IllegalArgumentException if [username] is empty, or [pendingIntent] is null
+ * are non null
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class PublicKeyCredentialEntry internal constructor(
+    typeDisplayName: CharSequence,
+    username: CharSequence,
+    displayName: CharSequence?,
+    pendingIntent: PendingIntent,
+    lastUsedTime: Long,
+    icon: Icon,
+    autoSelectAllowed: Boolean
+) : CredentialEntry(
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, typeDisplayName, username, displayName,
+    pendingIntent, lastUsedTime, icon, autoSelectAllowed
+) {
+
+    /**
+     * Builder for [PublicKeyCredentialEntry]
+     *
+     * @hide
+     */
+    class Builder {
+        // TODO("Add autoSelect")
+        private val context: Context
+        private val username: CharSequence
+        private var displayName: CharSequence? = null
+        private var pendingIntent: PendingIntent? = null
+        private var lastUsedTimeMillis: Long = 0
+        private var icon: Icon? = null
+        private var autoSelectAllowed: Boolean = false
+
+        /**
+         * @param username the username of the account holding the credential
+         * @param pendingIntent the [PendingIntent] to be invoked when the entry is selected
+         *
+         * Providers should use this constructor when an additional activity is required
+         * before returning the final [PasswordCredential]
+         */
+        constructor(context: Context, username: CharSequence, pendingIntent: PendingIntent) {
+            this.context = context
+            this.username = username
+            this.pendingIntent = pendingIntent
+        }
+
+        /** Sets a displayName to be shown on the UI with this entry */
+        fun setDisplayName(displayName: CharSequence?): Builder {
+            this.displayName = displayName
+            return this
+        }
+
+        /** Sets the icon to be shown on the UI with this entry */
+        fun setIcon(icon: Icon?): Builder {
+            this.icon = icon
+            return this
+        }
+        /**
+         * Sets whether the entry should be auto-selected.
+         * The value is fale by default
+         */
+        @Suppress("MissingGetterMatchingBuilder")
+        fun setAutoSelectAllowed(autoSelectAllowed: Boolean): Builder {
+            this.autoSelectAllowed = autoSelectAllowed
+            return this
+        }
+
+        /**
+         * Sets the last used time of this account
+         *
+         * This information will be used to sort the entries on the selector.
+         */
+        fun setLastUsedTimeMillis(lastUsedTimeMillis: Long): Builder {
+            this.lastUsedTimeMillis = lastUsedTimeMillis
+            return this
+        }
+
+        /** Builds an instance of [PublicKeyCredentialEntry] */
+        fun build(): PublicKeyCredentialEntry {
+            if (icon == null) {
+                icon = Icon.createWithResource(context, R.drawable.ic_password)
+            }
+            val typeDisplayName = context.getString(
+                R.string.androidx_credentials_TYPE_PUBLIC_KEY_CREDENTIAL)
+            return PublicKeyCredentialEntry(typeDisplayName,
+                username, displayName, pendingIntent!!,
+                lastUsedTimeMillis, icon!!, autoSelectAllowed)
+        }
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
new file mode 100644
index 0000000..86e82eb
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/provider/RemoteEntry.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.credentials.provider
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.net.Uri
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+import androidx.credentials.PublicKeyCredential
+import androidx.credentials.provider.Action.Companion.toSlice
+import java.util.Collections
+
+/**
+ * An entry on the selector, denoting that the credential will be retrieved from a remote device.
+ *
+ * @property pendingIntent the [PendingIntent] to be invoked when the user selects
+ * this entry
+ *
+ * See [CredentialsResponseContent] for usage details.
+ *
+ * @hide
+ */
+@RequiresApi(34)
+class RemoteEntry constructor(
+    // TODO("Add a PublicKeyRemoteEntry as a derived class and set the type there")
+    val pendingIntent: PendingIntent,
+    val type: String = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+    ) {
+    companion object {
+        private const val TAG = "RemoteEntry"
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_TYPE =
+            "androidx.credentials.provider.remoteEntry.SLICE_HINT_TYPE"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        internal const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.remoteEntry.SLICE_HINT_PENDING_INTENT"
+        @JvmStatic
+        fun toSlice(remoteEntry: RemoteEntry): Slice {
+            // TODO("Put the right spec and version value")
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(remoteEntry.type, 1))
+            sliceBuilder.addAction(remoteEntry.pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null)
+            return sliceBuilder.build()
+        }
+
+        @JvmStatic
+        internal fun toFrameworkCredentialEntryClass(remoteEntry: RemoteEntry):
+            android.service.credentials.CredentialEntry {
+            return android.service.credentials.CredentialEntry.Builder(
+                remoteEntry.type,
+                toSlice(remoteEntry),
+                remoteEntry.pendingIntent).build()
+        }
+
+        @JvmStatic
+        internal fun toFrameworkCreateEntryClass(remoteEntry: RemoteEntry):
+            android.service.credentials.CreateEntry {
+            return android.service.credentials.CreateEntry(
+                toSlice(remoteEntry),
+                remoteEntry.pendingIntent)
+        }
+
+        /**
+         * Returns an instance of [RemoteEntry] derived from a [Slice] object.
+         *
+         * @param slice the [Slice] object constructed through [toSlice]
+         */
+        @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
+        @JvmStatic
+        fun fromSlice(slice: Slice): RemoteEntry? {
+            val type = slice.spec!!.type
+            slice.items.forEach {
+                if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
+                    return try {
+                        RemoteEntry(it.action, type)
+                    } catch (e: Exception) {
+                        Log.i(TAG, "fromSlice failed with: " + e.message)
+                        null
+                    }
+                }
+            }
+            return null
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/drawable/ic_other_sign_in.xml b/credentials/credentials/src/main/res/drawable/ic_other_sign_in.xml
new file mode 100644
index 0000000..d1bb37e
--- /dev/null
+++ b/credentials/credentials/src/main/res/drawable/ic_other_sign_in.xml
@@ -0,0 +1,36 @@
+<!--
+  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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:name="path"
+        android:pathData="M 20 19 L 12 19 L 12 21 L 20 21 C 21.1 21 22 20.1 22 19 L 22 5 C 22 3.9 21.1 3 20 3 L 12 3 L 12 5 L 20 5 L 20 19 Z"
+        android:fillColor="#000"
+        android:strokeWidth="1"/>
+    <path
+        android:name="path_1"
+        android:pathData="M 12 7 L 10.6 8.4 L 13.2 11 L 8.85 11 C 8.42 9.55 7.09 8.5 5.5 8.5 C 3.57 8.5 2 10.07 2 12 C 2 13.93 3.57 15.5 5.5 15.5 C 7.09 15.5 8.42 14.45 8.85 13 L 13.2 13 L 10.6 15.6 L 12 17 L 17 12 L 12 7 Z M 5.5 13.5 C 4.67 13.5 4 12.83 4 12 C 4 11.17 4.67 10.5 5.5 10.5 C 6.33 10.5 7 11.17 7 12 C 7 12.83 6.33 13.5 5.5 13.5 Z"
+        android:fillColor="#000"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/drawable/ic_passkey.xml b/credentials/credentials/src/main/res/drawable/ic_passkey.xml
new file mode 100644
index 0000000..9c4304e
--- /dev/null
+++ b/credentials/credentials/src/main/res/drawable/ic_passkey.xml
@@ -0,0 +1,32 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="28dp"
+    android:height="24dp"
+    android:viewportWidth="28"
+    android:viewportHeight="24">
+    <path
+        android:pathData="M27.453,13.253C27.453,14.952 26.424,16.411 24.955,17.041L26.21,18.295L24.839,19.666L26.21,21.037L23.305,23.942L22.012,22.65L22.012,17.156C20.385,16.605 19.213,15.066 19.213,13.253C19.213,10.977 21.058,9.133 23.333,9.133C25.609,9.133 27.453,10.977 27.453,13.253ZM25.47,13.254C25.47,14.434 24.514,15.39 23.334,15.39C22.154,15.39 21.197,14.434 21.197,13.254C21.197,12.074 22.154,11.118 23.334,11.118C24.514,11.118 25.47,12.074 25.47,13.254Z"
+        android:fillColor="#00639B"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="M17.85,5.768C17.85,8.953 15.268,11.536 12.083,11.536C8.897,11.536 6.315,8.953 6.315,5.768C6.315,2.582 8.897,0 12.083,0C15.268,0 17.85,2.582 17.85,5.768Z"
+        android:fillColor="#00639B"/>
+    <path
+        android:pathData="M0.547,20.15C0.547,16.32 8.23,14.382 12.083,14.382C13.59,14.382 15.684,14.679 17.674,15.269C18.116,16.454 18.952,17.447 20.022,18.089V23.071H0.547V20.15Z"
+        android:fillColor="#00639B"/>
+</vector>
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/drawable/ic_password.xml b/credentials/credentials/src/main/res/drawable/ic_password.xml
new file mode 100644
index 0000000..1fb71cf
--- /dev/null
+++ b/credentials/credentials/src/main/res/drawable/ic_password.xml
@@ -0,0 +1,31 @@
+<!--
+  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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:name="path"
+        android:pathData="M 8.71 10.29 C 8.52 10.1 8.28 10 8 10 L 7.75 10 L 7.75 8.75 C 7.75 7.98 7.48 7.33 6.95 6.8 C 6.42 6.27 5.77 6 5 6 C 4.23 6 3.58 6.27 3.05 6.8 C 2.52 7.33 2.25 7.98 2.25 8.75 L 2.25 10 L 2 10 C 1.72 10 1.48 10.1 1.29 10.29 C 1.1 10.48 1 10.72 1 11 L 1 16 C 1 16.28 1.1 16.52 1.29 16.71 C 1.48 16.9 1.72 17 2 17 L 8 17 C 8.28 17 8.52 16.9 8.71 16.71 C 8.9 16.52 9 16.28 9 16 L 9 11 C 9 10.72 8.9 10.48 8.71 10.29 Z M 6.25 10 L 3.75 10 L 3.75 8.75 C 3.75 8.4 3.87 8.1 4.11 7.86 C 4.35 7.62 4.65 7.5 5 7.5 C 5.35 7.5 5.65 7.62 5.89 7.86 C 6.13 8.1 6.25 8.4 6.25 8.75 L 6.25 10 Z M 10 14 L 23 14 L 23 16 L 10 16 Z M 21.5 9 C 21.102 9 20.721 9.158 20.439 9.439 C 20.158 9.721 20 10.102 20 10.5 C 20 10.898 20.158 11.279 20.439 11.561 C 20.721 11.842 21.102 12 21.5 12 C 21.898 12 22.279 11.842 22.561 11.561 C 22.842 11.279 23 10.898 23 10.5 C 23 10.102 22.842 9.721 22.561 9.439 C 22.279 9.158 21.898 9 21.5 9 Z M 16.5 9 C 16.102 9 15.721 9.158 15.439 9.439 C 15.158 9.721 15 10.102 15 10.5 C 15 10.898 15.158 11.279 15.439 11.561 C 15.721 11.842 16.102 12 16.5 12 C 16.898 12 17.279 11.842 17.561 11.561 C 17.842 11.279 18 10.898 18 10.5 C 18 10.102 17.842 9.721 17.561 9.439 C 17.279 9.158 16.898 9 16.5 9 Z M 11.5 9 C 11.102 9 10.721 9.158 10.439 9.439 C 10.158 9.721 10 10.102 10 10.5 C 10 10.898 10.158 11.279 10.439 11.561 C 10.721 11.842 11.102 12 11.5 12 C 11.898 12 12.279 11.842 12.561 11.561 C 12.842 11.279 13 10.898 13 10.5 C 13 10.102 12.842 9.721 12.561 9.439 C 12.279 9.158 11.898 9 11.5 9 Z"
+        android:fillColor="#000"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/credentials/credentials/src/main/res/values-af/strings.xml b/credentials/credentials/src/main/res/values-af/strings.xml
new file mode 100644
index 0000000..324d169
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-af/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Wagwoordsleutel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Wagwoord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ar/strings.xml b/credentials/credentials/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000..0af8c23
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ar/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"مفتاح المرور"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"كلمة المرور"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-as/strings.xml b/credentials/credentials/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..54f51cb
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-as/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"পাছকী"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"পাছৱৰ্ড"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-az/strings.xml b/credentials/credentials/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000..8c1756f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-az/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Giriş açarı"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parol"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-b+sr+Latn/strings.xml b/credentials/credentials/src/main/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..0e0be5e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Приступни кôд"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Лозинка"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-be/strings.xml b/credentials/credentials/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000..4bd1021
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-be/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ключ доступу"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Пароль"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-bg/strings.xml b/credentials/credentials/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000..262756f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-bg/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Код за достъп"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Парола"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-bn/strings.xml b/credentials/credentials/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000..ba552e9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-bn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"পাসকী"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"পাসওয়ার্ড"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-bs/strings.xml b/credentials/credentials/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000..d1bd317
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-bs/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pristupni ključ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Lozinka"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ca/strings.xml b/credentials/credentials/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000..ee5660e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ca/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clau d\'accés"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contrasenya"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-cs/strings.xml b/credentials/credentials/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000..6d618e8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-cs/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Přístupový klíč"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Heslo"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-da/strings.xml b/credentials/credentials/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000..f8c9e29
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-da/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Adgangsnøgle"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Adgangskode"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-de/strings.xml b/credentials/credentials/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000..49d54d2
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-de/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Passwort"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-el/strings.xml b/credentials/credentials/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000..5c2d73e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-el/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Κλειδί πρόσβασης"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Κωδικός πρόσβασης"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rAU/strings.xml b/credentials/credentials/src/main/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rAU/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rCA/strings.xml b/credentials/credentials/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rGB/strings.xml b/credentials/credentials/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rIN/strings.xml b/credentials/credentials/src/main/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rIN/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-en-rXC/strings.xml b/credentials/credentials/src/main/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..ee94f40
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-en-rXC/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‎‎‎‏‏‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‏‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‎Passkey‎‏‎‎‏‎"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‎‎‏‎‏‏‎‏‏‏‎‎‏‏‎‏‎‏‎‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎Password‎‏‎‎‏‎"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-es-rUS/strings.xml b/credentials/credentials/src/main/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..898ed7d9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-es-rUS/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Llave de acceso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contraseña"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-es/strings.xml b/credentials/credentials/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000..898ed7d9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-es/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Llave de acceso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contraseña"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-et/strings.xml b/credentials/credentials/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000..294c693
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-et/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pääsuvõti"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parool"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-eu/strings.xml b/credentials/credentials/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000..b1db172
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-eu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Sarbide-gakoa"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Pasahitza"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fa/strings.xml b/credentials/credentials/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000..f1b8f5f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"گذرکلید"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"گذرواژه"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fi/strings.xml b/credentials/credentials/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000..8adce03
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Avainkoodi"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Salasana"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fr-rCA/strings.xml b/credentials/credentials/src/main/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..555784b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fr-rCA/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clé d\'accès"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Mot de passe"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-fr/strings.xml b/credentials/credentials/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000..555784b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-fr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clé d\'accès"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Mot de passe"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-gl/strings.xml b/credentials/credentials/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000..6bf8e92
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-gl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Clave de acceso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Contrasinal"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-gu/strings.xml b/credentials/credentials/src/main/res/values-gu/strings.xml
new file mode 100644
index 0000000..d63c6d9
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-gu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"પાસકી"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"પાસવર્ડ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hi/strings.xml b/credentials/credentials/src/main/res/values-hi/strings.xml
new file mode 100644
index 0000000..bbcc98c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"पासकी"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"पासवर्ड"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hr/strings.xml b/credentials/credentials/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000..6edcb91
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Pristupni ključ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Zaporka"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hu/strings.xml b/credentials/credentials/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000..c0e0cb3
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Azonosítókulcs"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Jelszó"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-hy/strings.xml b/credentials/credentials/src/main/res/values-hy/strings.xml
new file mode 100644
index 0000000..617300a0
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-hy/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Անցաբառ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Գաղտնաբառ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-in/strings.xml b/credentials/credentials/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000..1683197
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-in/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kunci sandi"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Sandi"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-is/strings.xml b/credentials/credentials/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000..e98a644
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-is/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Aðgangslykill"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Aðgangsorð"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-it/strings.xml b/credentials/credentials/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-it/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ja/strings.xml b/credentials/credentials/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000..2e926be
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ja/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"パスキー"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"パスワード"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ka/strings.xml b/credentials/credentials/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000..40c308b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ka/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"წვდომის გასაღები"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"პაროლი"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-kk/strings.xml b/credentials/credentials/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000..592eff2
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-kk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Кіру кілті"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Құпия сөз"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-km/strings.xml b/credentials/credentials/src/main/res/values-km/strings.xml
new file mode 100644
index 0000000..0aa413a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-km/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"កូដសម្ងាត់"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ពាក្យសម្ងាត់"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-kn/strings.xml b/credentials/credentials/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000..c57f6209
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-kn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ಪಾಸ್‌ಕೀ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ಪಾಸ್‌ವರ್ಡ್"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ko/strings.xml b/credentials/credentials/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000..dc494ec
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ko/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"패스키"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"비밀번호"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ky/strings.xml b/credentials/credentials/src/main/res/values-ky/strings.xml
new file mode 100644
index 0000000..3366129c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ky/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Мүмкүндүк алуу ачкычы"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Сырсөз"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-lo/strings.xml b/credentials/credentials/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000..d642614
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-lo/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ກະແຈຜ່ານ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ລະຫັດຜ່ານ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-lt/strings.xml b/credentials/credentials/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000..ee87b96
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-lt/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Slaptažodis"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-lv/strings.xml b/credentials/credentials/src/main/res/values-lv/strings.xml
new file mode 100644
index 0000000..9d79e3a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-lv/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Piekļuves atslēga"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parole"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-mk/strings.xml b/credentials/credentials/src/main/res/values-mk/strings.xml
new file mode 100644
index 0000000..96f6efe
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-mk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Криптографски клуч"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Лозинка"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ml/strings.xml b/credentials/credentials/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000..e272342
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ml/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"പാസ്‌കീ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"പാസ്‌വേഡ്"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-mn/strings.xml b/credentials/credentials/src/main/res/values-mn/strings.xml
new file mode 100644
index 0000000..59e9ded
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-mn/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Нууц үг"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-mr/strings.xml b/credentials/credentials/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000..bbcc98c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-mr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"पासकी"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"पासवर्ड"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ms/strings.xml b/credentials/credentials/src/main/res/values-ms/strings.xml
new file mode 100644
index 0000000..aacb31b
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ms/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kunci laluan"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Kata Laluan"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-my/strings.xml b/credentials/credentials/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000..3ea63a4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-my/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"လျှို့ဝှက်ကီး"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"စကားဝှက်"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-nb/strings.xml b/credentials/credentials/src/main/res/values-nb/strings.xml
new file mode 100644
index 0000000..a72318a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-nb/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Tilgangsnøkkel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Passord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ne/strings.xml b/credentials/credentials/src/main/res/values-ne/strings.xml
new file mode 100644
index 0000000..bbcc98c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ne/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"पासकी"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"पासवर्ड"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-nl/strings.xml b/credentials/credentials/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..db3ba80
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-nl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Toegangssleutel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Wachtwoord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-or/strings.xml b/credentials/credentials/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..6f31314
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-or/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ପାସକୀ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ପାସୱାର୍ଡ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pa/strings.xml b/credentials/credentials/src/main/res/values-pa/strings.xml
new file mode 100644
index 0000000..ca4c10a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pa/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"ਪਾਸਕੀ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"ਪਾਸਵਰਡ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pl/strings.xml b/credentials/credentials/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000..7c4d4c1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Klucz"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Hasło"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pt-rBR/strings.xml b/credentials/credentials/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..554e9b8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Chave de acesso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Senha"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pt-rPT/strings.xml b/credentials/credentials/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..f405d93
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Chave de acesso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Palavra-passe"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-pt/strings.xml b/credentials/credentials/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..554e9b8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-pt/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Chave de acesso"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Senha"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ro/strings.xml b/credentials/credentials/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000..9748df0
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ro/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Cheie de acces"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parolă"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ru/strings.xml b/credentials/credentials/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000..d3e3ce4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ru/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ключ доступа"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Пароль"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-si/strings.xml b/credentials/credentials/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000..4eee3ad
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-si/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"මුරයතුර"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"මුරපදය"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sk/strings.xml b/credentials/credentials/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000..f92dc9a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Prístupový kľúč"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Heslo"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sl/strings.xml b/credentials/credentials/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000..38907ef6
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ključ za dostop"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Geslo"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sq/strings.xml b/credentials/credentials/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000..00f2ae3
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sq/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Çelësi i kalimit"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Fjalëkalimi"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sr/strings.xml b/credentials/credentials/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000..0e0be5e
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Приступни кôд"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Лозинка"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sv/strings.xml b/credentials/credentials/src/main/res/values-sv/strings.xml
new file mode 100644
index 0000000..808fd8c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sv/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Nyckel"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Lösenord"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-sw/strings.xml b/credentials/credentials/src/main/res/values-sw/strings.xml
new file mode 100644
index 0000000..a129b58
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-sw/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ufunguo wa siri"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Nenosiri"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ta/strings.xml b/credentials/credentials/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000..458bcb4
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ta/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"கடவுக்குறியீடு"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"கடவுச்சொல்"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-te/strings.xml b/credentials/credentials/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000..67ad9ab
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-te/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"పాస్-కీ"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"పాస్‌వర్డ్"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-th/strings.xml b/credentials/credentials/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000..e2b685f
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-th/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"พาสคีย์"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"รหัสผ่าน"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-tl/strings.xml b/credentials/credentials/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000..b9f9ba1
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-tl/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Passkey"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Password"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-tr/strings.xml b/credentials/credentials/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000..f00b298
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-tr/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Şifre anahtarı"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Şifre"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-uk/strings.xml b/credentials/credentials/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000..4bd1021
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-uk/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ключ доступу"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Пароль"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-ur/strings.xml b/credentials/credentials/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000..3183ec3
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-ur/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"پاس کی"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"پاس ورڈ"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-uz/strings.xml b/credentials/credentials/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000..7f1bb8c
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-uz/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Kod"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Parol"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-vi/strings.xml b/credentials/credentials/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000..28a4e5a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-vi/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Mã xác thực"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Mật khẩu"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zh-rCN/strings.xml b/credentials/credentials/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..8f3d028
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"通行密钥"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"密码"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zh-rHK/strings.xml b/credentials/credentials/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..884239a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"密碼金鑰"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"密碼"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zh-rTW/strings.xml b/credentials/credentials/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..884239a
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"密碼金鑰"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"密碼"</string>
+</resources>
diff --git a/credentials/credentials/src/main/res/values-zu/strings.xml b/credentials/credentials/src/main/res/values-zu/strings.xml
new file mode 100644
index 0000000..f14c8e8
--- /dev/null
+++ b/credentials/credentials/src/main/res/values-zu/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" msgid="3929015085059320822">"Ukhiye wokudlula"</string>
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL" msgid="8397015543330865059">"Iphasiwedi"</string>
+</resources>
diff --git a/window/window-samples/src/main/res/values/colors.xml b/credentials/credentials/src/main/res/values/strings.xml
similarity index 60%
copy from window/window-samples/src/main/res/values/colors.xml
copy to credentials/credentials/src/main/res/values/strings.xml
index 41a72b2..2fef094 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/credentials/credentials/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2020 The Android Open Source Project
+  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.
@@ -15,13 +15,9 @@
   limitations under the License.
   -->
 
-<resources>
-    <color name="colorPrimary">#6200EE</color>
-    <color name="colorPrimaryDark">#3700B3</color>
-    <color name="colorAccent">#03DAC5</color>
-
-    <color name="colorFeatureFold">#7700FF00</color>
-
-    <color name="colorSplitContentBackground">#3B6BDB4C</color>
-    <color name="colorSplitControlsBackground">#475ABFF3</color>
-</resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Credential type label for passkey -->
+    <string name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL">Passkey</string>
+    <!-- Credential type label for password -->
+    <string name="android.credentials.TYPE_PASSWORD_CREDENTIAL">Password</string>
+</resources>
\ No newline at end of file
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 6b8bba5..d780eaa 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -811,4 +811,6 @@
 # > Configure project :internal-testutils-ktx
 WARNING:The option setting 'android\.r8\.maxWorkers=[0-9]+' is experimental\.
 # Building XCFrameworks (b/260140834) and iOS benchmark invocation
-.*xcodebuild.*
\ No newline at end of file
+.*xcodebuild.*
+Observed package id 'platforms;android-33-ext4' in inconsistent location.*
+.*xcodebuild.*
diff --git a/development/studio/idea.properties b/development/studio/idea.properties
index f352237..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.
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 9ebbebc..bb6c954 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -248,6 +248,8 @@
     docs(project(":preference:preference"))
     docs(project(":preference:preference-ktx"))
     docs(project(":print:print"))
+    docs(project(":privacysandbox:ads:ads-adservices"))
+    docs(project(":privacysandbox:ads:ads-adservices-java"))
     docs(project(":privacysandbox:sdkruntime:sdkruntime-client"))
     docs(project(":privacysandbox:sdkruntime:sdkruntime-core"))
     docs(project(":privacysandbox:tools:tools"))
@@ -351,6 +353,7 @@
     docs(project(":wear:watchface:watchface-style"))
     docs(project(":webkit:webkit"))
     docs(project(":window:window"))
+    samples(project(":window:window-samples"))
     docs(project(":window:window-core"))
     docs(project(":window:window-java"))
     docs(project(":window:window-rxjava2"))
diff --git a/drawerlayout/drawerlayout/api/api_lint.ignore b/drawerlayout/drawerlayout/api/api_lint.ignore
index be4e831..69b398e 100644
--- a/drawerlayout/drawerlayout/api/api_lint.ignore
+++ b/drawerlayout/drawerlayout/api/api_lint.ignore
@@ -3,12 +3,6 @@
     Parameter type is concrete collection (`java.util.ArrayList`); must be higher-level interface
 
 
-InvalidNullabilityOverride: androidx.drawerlayout.widget.DrawerLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
-    Invalid nullability on parameter `canvas` in method `drawChild`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.drawerlayout.widget.DrawerLayout#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `c` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-
-
 ListenerInterface: androidx.drawerlayout.widget.DrawerLayout.SimpleDrawerListener:
     Listeners should be an interface, or otherwise renamed Callback: SimpleDrawerListener
 
@@ -23,6 +17,8 @@
     Missing nullability on parameter `p` in method `checkLayoutParams`
 MissingNullability: androidx.drawerlayout.widget.DrawerLayout#dispatchGenericMotionEvent(android.view.MotionEvent) parameter #0:
     Missing nullability on parameter `event` in method `dispatchGenericMotionEvent`
+MissingNullability: androidx.drawerlayout.widget.DrawerLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
+    Missing nullability on parameter `canvas` in method `drawChild`
 MissingNullability: androidx.drawerlayout.widget.DrawerLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #1:
     Missing nullability on parameter `child` in method `drawChild`
 MissingNullability: androidx.drawerlayout.widget.DrawerLayout#generateDefaultLayoutParams():
@@ -35,6 +31,8 @@
     Missing nullability on method `generateLayoutParams` return
 MissingNullability: androidx.drawerlayout.widget.DrawerLayout#generateLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
     Missing nullability on parameter `p` in method `generateLayoutParams`
+MissingNullability: androidx.drawerlayout.widget.DrawerLayout#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `c` in method `onDraw`
 MissingNullability: androidx.drawerlayout.widget.DrawerLayout#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
     Missing nullability on parameter `ev` in method `onInterceptTouchEvent`
 MissingNullability: androidx.drawerlayout.widget.DrawerLayout#onKeyDown(int, android.view.KeyEvent) parameter #1:
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
index f6e41bb..baa727c 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
@@ -167,9 +167,9 @@
     private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
     var paint: Paint? = null
 
-    override fun draw(canvas: Canvas?) {
+    override fun draw(canvas: Canvas) {
         super.draw(canvas)
-        canvas?.apply {
+        canvas.apply {
             paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
         }
     }
diff --git a/gradle.properties b/gradle.properties
index 599e535..9186349 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -23,10 +23,10 @@
 android.experimental.lint.missingBaselineIsEmptyBaseline=true
 
 # Don't generate versioned API files
-androidx.writeVersionedApiFiles=true
+androidx.writeVersionedApiFiles=false
 
-# Do restrict compileSdkPreview usage
-androidx.allowCustomCompileSdk=false
+# Don't restrict compileSdkPreview usage
+androidx.allowCustomCompileSdk=true
 
 # Don't warn about needing to update AGP
 android.suppressUnsupportedCompileSdk=Tiramisu,33
diff --git a/graphics/OWNERS b/graphics/OWNERS
index 9ba9c32..db046a2 100644
--- a/graphics/OWNERS
+++ b/graphics/OWNERS
@@ -1,4 +1,5 @@
 # Bug component: 1137062
 [email protected]
 [email protected]
[email protected]
[email protected]
[email protected]
\ No newline at end of file
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index 8a8757a..d346b3d 100644
--- a/health/connect/connect-client/build.gradle
+++ b/health/connect/connect-client/build.gradle
@@ -43,6 +43,7 @@
     implementation(libs.guavaAndroid)
     implementation(libs.kotlinCoroutinesAndroid)
     implementation(libs.kotlinCoroutinesGuava)
+    implementation("androidx.core:core-ktx:1.8.0")
 
     testImplementation(libs.testCore)
     testImplementation(libs.testRunner)
@@ -58,6 +59,13 @@
     testImplementation(libs.espressoIntents)
     testImplementation(libs.kotlinReflect)
 
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.kotlinCoroutinesTest)
+    androidTestImplementation(libs.kotlinReflect)
+    androidTestImplementation(libs.kotlinTest)
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.truth)
+
     samples(project(":health:connect:connect-client-samples"))
 }
 
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
new file mode 100644
index 0000000..bbd7551
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.health.connect.client.impl
+
+import android.annotation.TargetApi
+import android.os.Build
+import android.os.RemoteException
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.changes.UpsertionChange
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.Period
+import java.time.ZoneOffset
+import kotlin.test.assertFailsWith
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@MediumTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class HealthConnectClientUpsideDownImplTest {
+
+    private lateinit var healthConnectClient: HealthConnectClient
+
+    @Before
+    fun setUp() {
+        healthConnectClient =
+            HealthConnectClientUpsideDownImpl(ApplicationProvider.getApplicationContext())
+    }
+
+    @Test
+    fun filterGrantedPermissions_throwUOE() = runTest {
+        assertFailsWith<UnsupportedOperationException> {
+            healthConnectClient.permissionController.filterGrantedPermissions(
+                setOf(HealthPermission.READ_ACTIVE_CALORIES_BURNED)
+            )
+        }
+    }
+
+    @Test
+    fun getGrantedPermission_throwUOE() = runTest {
+        assertFailsWith<UnsupportedOperationException> {
+            healthConnectClient.permissionController.getGrantedPermissions(
+                setOf(
+                    HealthPermission.createReadPermission(
+                        StepsRecord::class,
+                    )
+                )
+            )
+        }
+    }
+
+    @Test
+    fun insertRecords() = runTest {
+        val response = healthConnectClient.insertRecords(
+            listOf(
+                StepsRecord(
+                    count = 100,
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(5678L),
+                    endZoneOffset = null
+                )
+            )
+        )
+        assertThat(response.recordIdsList).hasSize(1)
+    }
+
+    @Test
+    fun updateRecords() = runTest {
+        val id = healthConnectClient.insertRecords(
+            listOf(
+                StepsRecord(
+                    count = 100,
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(5678L),
+                    endZoneOffset = null
+                )
+            )
+        ).recordIdsList[0]
+
+        val insertedRecord = healthConnectClient.readRecord(StepsRecord::class, id).record
+
+        healthConnectClient.updateRecords(
+            listOf(
+                StepsRecord(
+                    count = 50,
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = null,
+                    endTime = Instant.ofEpochMilli(5678L),
+                    endZoneOffset = null,
+                    metadata = Metadata(
+                        id,
+                        insertedRecord.metadata.dataOrigin
+                    )
+                )
+            )
+        )
+
+        val updatedRecord = healthConnectClient.readRecord(StepsRecord::class, id).record
+
+        assertThat(updatedRecord.count).isEqualTo(50L)
+    }
+
+    @Test
+    fun readRecord_withId() = runTest {
+        val insertResponse = healthConnectClient.insertRecords(
+            listOf(
+                StepsRecord(
+                    count = 100,
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endTime = Instant.ofEpochMilli(5678L),
+                    endZoneOffset = ZoneOffset.UTC
+                )
+            )
+        )
+
+        val readResponse =
+            healthConnectClient.readRecord(StepsRecord::class, insertResponse.recordIdsList[0])
+
+        with(readResponse.record) {
+            assertThat(count).isEqualTo(100)
+            assertThat(startTime).isEqualTo(Instant.ofEpochMilli(1234L))
+            assertThat(startZoneOffset).isEqualTo(ZoneOffset.UTC)
+            assertThat(endTime).isEqualTo(Instant.ofEpochMilli(5678L))
+            assertThat(endZoneOffset).isEqualTo(ZoneOffset.UTC)
+        }
+    }
+
+    @Test
+    fun readRecords_withFilters() = runTest {
+        healthConnectClient.insertRecords(
+            listOf(
+                StepsRecord(
+                    count = 100,
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endTime = Instant.ofEpochMilli(5678L),
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+                StepsRecord(
+                    count = 50,
+                    startTime = Instant.ofEpochMilli(12340L),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endTime = Instant.ofEpochMilli(56780L),
+                    endZoneOffset = ZoneOffset.UTC
+                ),
+            )
+        )
+
+        val readResponse = healthConnectClient.readRecords(
+            ReadRecordsRequest(
+                StepsRecord::class,
+                TimeRangeFilter.after(Instant.ofEpochMilli(10_000L))
+            )
+        )
+
+        assertThat(readResponse.records[0].count).isEqualTo(50)
+    }
+
+    @Test
+    fun readRecord_noRecords_throwRemoteException() = runTest {
+        assertFailsWith<RemoteException> {
+            healthConnectClient.readRecord(StepsRecord::class, "1")
+        }
+    }
+
+    @Test
+    fun aggregateRecords_throwUOE() = runTest {
+        assertFailsWith<UnsupportedOperationException> {
+            healthConnectClient.aggregate(
+                AggregateRequest(
+                    setOf(StepsRecord.COUNT_TOTAL),
+                    TimeRangeFilter.between(
+                        Instant.ofEpochMilli(1234L),
+                        Instant.ofEpochMilli(1235L)
+                    )
+                )
+            )
+        }
+    }
+
+    @Test
+    fun aggregateRecordsGroupByDuration_throwUOE() = runTest {
+        assertFailsWith<UnsupportedOperationException> {
+            healthConnectClient.aggregateGroupByDuration(
+                AggregateGroupByDurationRequest(
+                    setOf(StepsRecord.COUNT_TOTAL),
+                    TimeRangeFilter.between(
+                        Instant.ofEpochMilli(1234L),
+                        Instant.ofEpochMilli(1235L)
+                    ),
+                    timeRangeSlicer = Duration.ofMillis(1)
+                )
+            )
+        }
+    }
+
+    @Test
+    fun aggregateRecordsGroupByPeriod_throwUOE() = runTest {
+        assertFailsWith<UnsupportedOperationException> {
+            healthConnectClient.aggregateGroupByPeriod(
+                AggregateGroupByPeriodRequest(
+                    setOf(StepsRecord.COUNT_TOTAL),
+                    TimeRangeFilter.between(
+                        LocalDateTime.of(2018, 10, 11, 7, 10),
+                        LocalDateTime.of(2018, 10, 13, 7, 10),
+                    ),
+                    timeRangeSlicer = Period.ofDays(1)
+                )
+            )
+        }
+    }
+
+    @Test
+    fun getChangesToken() = runTest {
+        val token = healthConnectClient.getChangesToken(
+            ChangesTokenRequest(
+                setOf(StepsRecord::class),
+                setOf()
+            )
+        )
+        assertThat(token).isNotEmpty()
+    }
+
+    @Test
+    fun getChanges() = runTest {
+        val token = healthConnectClient.getChangesToken(
+            ChangesTokenRequest(
+                setOf(StepsRecord::class),
+                setOf()
+            )
+        )
+
+        val insertResponse = healthConnectClient.insertRecords(
+            listOf(
+                StepsRecord(
+                    count = 100,
+                    startTime = Instant.ofEpochMilli(1234L),
+                    startZoneOffset = ZoneOffset.UTC,
+                    endTime = Instant.ofEpochMilli(5678L),
+                    endZoneOffset = ZoneOffset.UTC
+                )
+            )
+        )
+
+        // TODO(b/262240293): delete a record to test DeletionChange conversion
+
+        val record = healthConnectClient.readRecord(
+            StepsRecord::class,
+            insertResponse.recordIdsList[0]
+        ).record
+
+        assertThat(healthConnectClient.getChanges(token).changes).containsExactly(
+            UpsertionChange(
+                record
+            )
+        )
+    }
+}
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordsConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordsConvertersTest.kt
new file mode 100644
index 0000000..74b7ed8
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RecordsConvertersTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.health.connect.client.impl.platform.records
+
+import android.annotation.TargetApi
+import android.healthconnect.datatypes.DataOrigin
+import android.healthconnect.datatypes.Device
+import android.healthconnect.datatypes.Metadata
+import android.healthconnect.datatypes.StepsRecord
+import android.os.Build
+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 java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+import androidx.health.connect.client.records.StepsRecord as SdkStepsRecord
+import androidx.health.connect.client.records.metadata.Device as SdkDevice
+import androidx.health.connect.client.records.metadata.DataOrigin as SdkDataOrigin
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class RecordsConvertersTest {
+
+    @Test
+    fun stepsRecord_convertFromSdkToPlatformClass() {
+        val stepsSdkClass = SdkStepsRecord::class
+        val stepsPlatformClass = StepsRecord::class.java
+
+        assertThat(stepsSdkClass.toPlatformRecordClass()).isEqualTo(stepsPlatformClass)
+    }
+
+    @Test
+    fun stepsRecord_convertFromSdkToPlatform() {
+        val steps = SdkStepsRecord(
+            count = 100,
+            startTime = Instant.ofEpochMilli(1234L),
+            startZoneOffset = null,
+            endTime = Instant.ofEpochMilli(5678L),
+            endZoneOffset = null
+        )
+
+        val platformSteps = steps.toPlatformRecord() as StepsRecord
+
+        assertThat(platformSteps.count).isEqualTo(100)
+        assertThat(platformSteps.startTime).isEqualTo(Instant.ofEpochMilli(1234L))
+        assertThat(platformSteps.endTime).isEqualTo(Instant.ofEpochMilli(5678L))
+    }
+
+    @Test
+    fun stepsRecord_convertFromPlatformToSdk() {
+        val steps = StepsRecord.Builder(Metadata.Builder().apply {
+            setDevice(Device.Builder().setType(Device.DEVICE_TYPE_WATCH).build())
+            setClientRecordVersion(123L)
+            setDataOrigin(DataOrigin.Builder().setPackageName("com.packageName").build())
+            setLastModifiedTime(Instant.ofEpochMilli(9999L))
+        }.build(), Instant.ofEpochMilli(5678L), Instant.ofEpochMilli(9012L), 200).build()
+
+        val sdkSteps = steps.toSdkRecord() as SdkStepsRecord
+
+        with(sdkSteps) {
+            assertThat(count).isEqualTo(200)
+            assertThat(startTime).isEqualTo(Instant.ofEpochMilli(5678L))
+            assertThat(endTime).isEqualTo(Instant.ofEpochMilli(9012L))
+
+            with(metadata) {
+                assertThat(device).isEqualTo(
+                    SdkDevice(
+                        type = Device.DEVICE_TYPE_WATCH
+                    )
+                )
+                assertThat(clientRecordVersion).isEqualTo(123L)
+                assertThat(dataOrigin).isEqualTo(
+                    SdkDataOrigin(
+                        "com.packageName"
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun dataOrigin_convertFromPlatformToSdk_nullableConvertsToEmptyString() {
+        val platformDataOrigin = DataOrigin.Builder().setPackageName(null).build()
+        assertThat(platformDataOrigin.toSdkDataOrigin()).isEqualTo(SdkDataOrigin(""))
+    }
+}
\ No newline at end of file
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
new file mode 100644
index 0000000..9aeb0b3
--- /dev/null
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/records/RequestConvertersTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.health.connect.client.impl.platform.records
+
+import android.healthconnect.datatypes.DataOrigin as PlatformDataOrigin
+import android.healthconnect.datatypes.StepsRecord as PlatformStepsRecord
+import android.annotation.TargetApi
+import android.os.Build
+import androidx.health.connect.client.impl.platform.time.SystemDefaultTimeSource
+import androidx.health.connect.client.impl.platform.time.FakeTimeSource
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import android.healthconnect.datatypes.HeartRateRecord as PlatformHeartRateRecord
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+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 java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@SmallTest
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+// Comment the SDK suppress to run on emulators lower than U.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class RequestConvertersTest {
+
+    @Test
+    fun readRecordsRequest_fromSdkToPlatform() {
+        val sdkRequest = ReadRecordsRequest(
+            StepsRecord::class,
+            TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L)),
+            setOf(DataOrigin("package1"), DataOrigin("package2"))
+        )
+
+        with(sdkRequest.toPlatformReadRecordsRequestUsingFilters(SystemDefaultTimeSource)) {
+            assertThat(recordType).isAssignableTo(PlatformStepsRecord::class.java)
+            assertThat(dataOrigins).containsExactly(
+                PlatformDataOrigin.Builder().setPackageName("package1").build(),
+                PlatformDataOrigin.Builder().setPackageName("package2").build()
+            )
+        }
+    }
+
+    @Test
+    fun changesTokenRequest_fromSdkToPlatform() {
+        val sdkRequest = ChangesTokenRequest(
+            setOf(StepsRecord::class, HeartRateRecord::class),
+            setOf(DataOrigin("package1"), DataOrigin("package2"))
+        )
+
+        with(sdkRequest.toPlatformChangeLogTokenRequest()) {
+            assertThat(recordTypes).containsExactly(
+                PlatformStepsRecord::class.java,
+                PlatformHeartRateRecord::class.java
+            )
+            assertThat(dataOriginFilters).containsExactly(
+                PlatformDataOrigin.Builder().setPackageName("package1").build(),
+                PlatformDataOrigin.Builder().setPackageName("package2").build()
+            )
+        }
+    }
+
+    @Test
+    fun timeRangeFilter_fromSdkToPlatform() {
+        val sdkFilter =
+            TimeRangeFilter.between(Instant.ofEpochMilli(123L), Instant.ofEpochMilli(456L))
+
+        with(sdkFilter.toPlatformTimeRangeFilter(SystemDefaultTimeSource)) {
+            assertThat(startTime).isEqualTo(Instant.ofEpochMilli(123L))
+            assertThat(endTime).isEqualTo(Instant.ofEpochMilli(456L))
+        }
+    }
+
+    @Test
+    fun timeRangeFilter_fromSdkToPlatform_none() {
+        val timeSource = FakeTimeSource()
+        timeSource.now = Instant.ofEpochMilli(123L)
+
+        val sdkFilter = TimeRangeFilter.none()
+
+        with(sdkFilter.toPlatformTimeRangeFilter(timeSource)) {
+            assertThat(startTime).isEqualTo(Instant.EPOCH)
+            assertThat(endTime).isEqualTo(Instant.ofEpochMilli(123L))
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/time/FakeTimeSource.kt
similarity index 68%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/time/FakeTimeSource.kt
index 7053e2d..270eaf4 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/time/FakeTimeSource.kt
@@ -14,6 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.health.connect.client.impl.platform.time
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.time.Instant
+
+@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class FakeTimeSource : TimeSource {
+    override lateinit var now: Instant
+}
\ No newline at end of file
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
new file mode 100644
index 0000000..9db6647
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImpl.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.health.connect.client.impl
+
+import android.content.Context
+import android.healthconnect.ChangeLogsRequest
+import android.healthconnect.HealthConnectException
+import android.healthconnect.HealthConnectManager
+import android.healthconnect.ReadRecordsRequestUsingIds
+import android.os.Build
+import android.os.RemoteException
+import androidx.annotation.RequiresApi
+import androidx.core.os.asOutcomeReceiver
+import androidx.health.connect.client.HealthConnectClient
+import androidx.health.connect.client.PermissionController
+import androidx.health.connect.client.aggregate.AggregationResult
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByDuration
+import androidx.health.connect.client.aggregate.AggregationResultGroupedByPeriod
+import androidx.health.connect.client.changes.DeletionChange
+import androidx.health.connect.client.changes.UpsertionChange
+import androidx.health.connect.client.impl.platform.records.toPlatformChangeLogTokenRequest
+import androidx.health.connect.client.impl.platform.records.toPlatformReadRecordsRequestUsingFilters
+import androidx.health.connect.client.impl.platform.records.toPlatformRecord
+import androidx.health.connect.client.impl.platform.records.toPlatformRecordClass
+import androidx.health.connect.client.impl.platform.records.toSdkRecord
+import androidx.health.connect.client.impl.platform.response.toKtResponse
+import androidx.health.connect.client.impl.platform.time.SystemDefaultTimeSource
+import androidx.health.connect.client.impl.platform.time.TimeSource
+import androidx.health.connect.client.impl.platform.toKtException
+import androidx.health.connect.client.permission.HealthPermission
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.request.AggregateGroupByDurationRequest
+import androidx.health.connect.client.request.AggregateGroupByPeriodRequest
+import androidx.health.connect.client.request.AggregateRequest
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.response.ChangesResponse
+import androidx.health.connect.client.response.InsertRecordsResponse
+import androidx.health.connect.client.response.ReadRecordResponse
+import androidx.health.connect.client.response.ReadRecordsResponse
+import androidx.health.connect.client.time.TimeRangeFilter
+import kotlin.reflect.KClass
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Implements the [HealthConnectClient] with APIs in UpsideDownCake.
+ *
+ * @suppress
+ */
+@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class HealthConnectClientUpsideDownImpl :
+    HealthConnectClient, PermissionController {
+
+    private val context: Context
+    private val timeSource: TimeSource
+    private val healthConnectManager: HealthConnectManager
+
+    constructor(context: Context) : this(context, SystemDefaultTimeSource)
+
+    internal constructor(context: Context, timeSource: TimeSource) {
+        this.context = context
+        this.timeSource = timeSource
+        this.healthConnectManager =
+            context.getSystemService(Context.HEALTHCONNECT_SERVICE) as HealthConnectManager
+    }
+
+    override val permissionController: PermissionController
+        get() = this
+
+    override suspend fun insertRecords(records: List<Record>): InsertRecordsResponse {
+        val response = wrapPlatformException {
+            suspendCancellableCoroutine<android.healthconnect.InsertRecordsResponse> { continuation
+                ->
+                healthConnectManager.insertRecords(
+                    records.map { it.toPlatformRecord() },
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+        return response.toKtResponse()
+    }
+
+    override suspend fun updateRecords(records: List<Record>) {
+        wrapPlatformException {
+            suspendCancellableCoroutine<Void> { continuation
+                ->
+                healthConnectManager.updateRecords(
+                    records.map { it.toPlatformRecord() },
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+    }
+
+    override suspend fun deleteRecords(
+        recordType: KClass<out Record>,
+        recordIdsList: List<String>,
+        clientRecordIdsList: List<String>
+    ) {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun deleteRecords(
+        recordType: KClass<out Record>,
+        timeRangeFilter: TimeRangeFilter
+    ) {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    @Suppress("UNCHECKED_CAST") // Safe to cast as the type should match
+    override suspend fun <T : Record> readRecord(
+        recordType: KClass<T>,
+        recordId: String
+    ): ReadRecordResponse<T> {
+        val response = wrapPlatformException {
+            suspendCancellableCoroutine { continuation
+                ->
+                healthConnectManager.readRecords(
+                    ReadRecordsRequestUsingIds
+                        .Builder(recordType.toPlatformRecordClass())
+                        .addId(recordId).build(),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+        if (response.records.isEmpty()) {
+            throw RemoteException("No records")
+        }
+        return ReadRecordResponse(response.records[0].toSdkRecord() as T)
+    }
+
+    @Suppress("UNCHECKED_CAST") // Safe to cast as the type should match
+    override suspend fun <T : Record> readRecords(
+        request: ReadRecordsRequest<T>
+    ): ReadRecordsResponse<T> {
+        val response = wrapPlatformException {
+            suspendCancellableCoroutine { continuation
+                ->
+                healthConnectManager.readRecords(
+                    request.toPlatformReadRecordsRequestUsingFilters(timeSource),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+        // TODO(b/262573513): pass page token
+        return ReadRecordsResponse(response.records.map { it.toSdkRecord() as T }, null)
+    }
+
+    override suspend fun aggregate(request: AggregateRequest): AggregationResult {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun aggregateGroupByDuration(
+        request: AggregateGroupByDurationRequest
+    ): List<AggregationResultGroupedByDuration> {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun aggregateGroupByPeriod(
+        request: AggregateGroupByPeriodRequest
+    ): List<AggregationResultGroupedByPeriod> {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun getChangesToken(request: ChangesTokenRequest): String {
+        return wrapPlatformException {
+            suspendCancellableCoroutine { continuation ->
+                healthConnectManager.getChangeLogToken(
+                    request.toPlatformChangeLogTokenRequest(),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+    }
+
+    override suspend fun registerForDataNotifications(
+        notificationIntentAction: String,
+        recordTypes: Iterable<KClass<out Record>>
+    ) {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun unregisterFromDataNotifications(notificationIntentAction: String) {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun getChanges(changesToken: String): ChangesResponse {
+        val response = wrapPlatformException {
+            suspendCancellableCoroutine { continuation ->
+                healthConnectManager.getChangeLogs(
+                    ChangeLogsRequest.Builder(changesToken).build(),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+        // TODO(b/263472286) revisit changesTokenExpired field in the constructor
+        return ChangesResponse(
+            buildList {
+                response.upsertedRecords.forEach { add(UpsertionChange(it.toSdkRecord())) }
+                response.deletedRecordIds.forEach { add(DeletionChange(it)) }
+            },
+            response.nextChangesToken,
+            response.hasMorePages(),
+            changesTokenExpired = true
+        )
+    }
+
+    override suspend fun getGrantedPermissions(
+        permissions: Set<HealthPermission>
+    ): Set<HealthPermission> {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun filterGrantedPermissions(permissions: Set<String>): Set<String> {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    override suspend fun revokeAllPermissions() {
+        throw UnsupportedOperationException("Method not supported yet")
+    }
+
+    internal suspend fun <T> wrapPlatformException(function: suspend () -> T): T {
+        return try {
+            function()
+        } catch (e: HealthConnectException) {
+            throw e.toKtException()
+        }
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt
new file mode 100644
index 0000000..ae79c6f
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/ExceptionConverter.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform
+
+import android.healthconnect.HealthConnectException
+import android.os.Build
+import android.os.RemoteException
+import androidx.annotation.RequiresApi
+import java.io.IOException
+import java.lang.IllegalArgumentException
+import java.lang.IllegalStateException
+
+/** Converts exception returned by the platform to one of standard exception class hierarchy. */
+internal fun HealthConnectException.toKtException(): Exception {
+    return when (errorCode) {
+        HealthConnectException.ERROR_IO -> IOException(message)
+        HealthConnectException.ERROR_REMOTE -> RemoteException(message)
+        HealthConnectException.ERROR_SECURITY -> SecurityException(message)
+        HealthConnectException.ERROR_INVALID_ARGUMENT -> IllegalArgumentException(message)
+        else -> IllegalStateException(message)
+    }
+}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/package-info.java
similarity index 67%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/package-info.java
index 7053e2d..dc32f56 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+/**
+ * Helps with conversions to the platform record and API objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.connect.client.impl.platform;
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordsConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordsConverters.kt
new file mode 100644
index 0000000..babbe8a
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RecordsConverters.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.healthconnect.datatypes.DataOrigin as PlatformDataOrigin
+import android.healthconnect.datatypes.Device as PlatformDevice
+import android.healthconnect.datatypes.Metadata as PlatformMetadata
+import android.healthconnect.datatypes.Record as PlatformRecord
+import android.healthconnect.datatypes.StepsRecord as PlatformStepsRecord
+import android.healthconnect.datatypes.HeartRateRecord as PlatformHeartRateRecord
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.metadata.DataOrigin
+import androidx.health.connect.client.records.metadata.Device
+import androidx.health.connect.client.records.metadata.Metadata
+import kotlin.reflect.KClass
+
+fun KClass<out Record>.toPlatformRecordClass():
+    Class<out PlatformRecord> {
+    return when (this) {
+        StepsRecord::class -> PlatformStepsRecord::class.java
+        HeartRateRecord::class -> PlatformHeartRateRecord::class.java
+        else -> throw IllegalArgumentException("Unsupported record type $this")
+    }
+}
+
+fun Record.toPlatformRecord(): PlatformRecord {
+    return when (this) {
+        is StepsRecord ->
+            PlatformStepsRecord.Builder(
+                metadata.toPlatformMetadata(),
+                startTime,
+                endTime,
+                count
+            )
+                .apply {
+                    startZoneOffset?.let { setStartZoneOffset(it) }
+                    endZoneOffset?.let { setEndZoneOffset(it) }
+                }
+                .build()
+
+        else -> throw IllegalArgumentException("Unsupported record $this")
+    }
+}
+
+fun PlatformRecord.toSdkRecord(): Record {
+    return when (this) {
+        is PlatformStepsRecord ->
+            StepsRecord(
+                startTime,
+                startZoneOffset,
+                endTime,
+                endZoneOffset,
+                count,
+                metadata.toSdkMetadata()
+            )
+
+        else -> throw IllegalArgumentException("Unsupported record $this")
+    }
+}
+
+fun Metadata.toPlatformMetadata(): PlatformMetadata {
+    return PlatformMetadata.Builder()
+        .apply {
+            device?.toPlatformDevice()?.let { setDevice(it) }
+            setLastModifiedTime(lastModifiedTime)
+            setId(id)
+            setDataOrigin(dataOrigin.toPlatformDataOrigin())
+            setClientRecordId(clientRecordId)
+            setClientRecordVersion(clientRecordVersion)
+        }
+        .build()
+}
+
+fun PlatformMetadata.toSdkMetadata(): Metadata {
+    return Metadata(
+        id,
+        dataOrigin.toSdkDataOrigin(),
+        lastModifiedTime,
+        clientRecordId,
+        clientRecordVersion,
+        device.toSdkDevice()
+    )
+}
+
+fun Device.toPlatformDevice(): PlatformDevice {
+    @Suppress("WrongConstant") // Platform intdef and jetpack intdef match in value.
+    return PlatformDevice.Builder()
+        .apply {
+            setType(type)
+            manufacturer?.let { setManufacturer(it) }
+            model?.let { setModel(it) }
+        }
+        .build()
+}
+
+fun PlatformDevice.toSdkDevice(): Device {
+    @Suppress("WrongConstant") // Platform intdef and jetpack intdef match in value.
+    return Device(manufacturer, model, type)
+}
+
+fun DataOrigin.toPlatformDataOrigin(): PlatformDataOrigin {
+    return PlatformDataOrigin.Builder()
+        .apply { setPackageName(packageName) }
+        .build()
+}
+
+fun PlatformDataOrigin.toSdkDataOrigin(): DataOrigin {
+    return DataOrigin(packageName ?: "")
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
new file mode 100644
index 0000000..1cbce0d
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/RequestConverters.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.records
+
+import android.healthconnect.TimeRangeFilter as PlatformTimeRangeFilter
+import android.healthconnect.datatypes.Record as PlatformRecord
+import android.healthconnect.ChangeLogTokenRequest
+import android.healthconnect.ReadRecordsRequestUsingFilters
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.impl.platform.time.TimeSource
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.request.ChangesTokenRequest
+import androidx.health.connect.client.request.ReadRecordsRequest
+import androidx.health.connect.client.time.TimeRangeFilter
+import java.time.Instant
+
+fun ReadRecordsRequest<out Record>.toPlatformReadRecordsRequestUsingFilters(
+    timeSource: TimeSource
+):
+    ReadRecordsRequestUsingFilters<out PlatformRecord> {
+    return ReadRecordsRequestUsingFilters
+        .Builder(recordType.toPlatformRecordClass())
+        .setTimeRangeFilter(timeRangeFilter.toPlatformTimeRangeFilter(timeSource))
+        .apply {
+            // TODO(b/262691771): revisit data origin filter once privacy decision is finalized
+            dataOriginFilter.forEach { addDataOrigins(it.toPlatformDataOrigin()) }
+        }
+        .build()
+}
+
+fun TimeRangeFilter.toPlatformTimeRangeFilter(
+    timeSource: TimeSource
+): PlatformTimeRangeFilter {
+    // TODO(b/262571990): pass nullable Instant start/end
+    // TODO(b/262571990): pass nullable LocalDateTime start/end
+    return PlatformTimeRangeFilter.Builder(startTime ?: Instant.EPOCH, endTime ?: timeSource.now)
+        .build()
+}
+
+fun ChangesTokenRequest.toPlatformChangeLogTokenRequest(): ChangeLogTokenRequest {
+    return ChangeLogTokenRequest.Builder()
+        .apply {
+            // TODO(b/262691771): revisit data origin filter once privacy decision is finalized
+            dataOriginFilters.forEach { addDataOriginFilter(it.toPlatformDataOrigin()) }
+            recordTypes.forEach { addRecordType(it.toPlatformRecordClass()) }
+        }
+        .build()
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/package-info.java
similarity index 66%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/package-info.java
index 7053e2d..c5b8a8e 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/records/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+/**
+ * Helps with conversions to the platform record and API objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.connect.client.impl.platform.records;
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/InsertRecordsResponseConverter.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/InsertRecordsResponseConverter.kt
new file mode 100644
index 0000000..348173e
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/InsertRecordsResponseConverter.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.response
+
+import android.healthconnect.InsertRecordsResponse
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+internal fun InsertRecordsResponse.toKtResponse():
+    androidx.health.connect.client.response.InsertRecordsResponse {
+    return androidx.health.connect.client.response.InsertRecordsResponse(
+        recordIdsList = records.map { record -> record.metadata.id }
+    )
+}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/package-info.java
similarity index 66%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/package-info.java
index 7053e2d..f8b9cb7 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/response/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+/**
+ * Helps with conversions to the platform record and API objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.connect.client.impl.platform.response;
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/time/TimeSource.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/time/TimeSource.kt
new file mode 100644
index 0000000..b0385c6
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/platform/time/TimeSource.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+@file:RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+
+package androidx.health.connect.client.impl.platform.time
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import java.time.Instant
+
+interface TimeSource {
+    val now: Instant
+}
+
+object SystemDefaultTimeSource : TimeSource {
+    override val now: Instant
+        get() = Instant.now()
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/api/api_lint.ignore b/heifwriter/heifwriter/api/api_lint.ignore
index a93261a..6c2b411 100644
--- a/heifwriter/heifwriter/api/api_lint.ignore
+++ b/heifwriter/heifwriter/api/api_lint.ignore
@@ -1,22 +1,23 @@
 // Baseline format: 1.0
+GenericException: androidx.heifwriter.AvifWriter#stop(long):
+    Methods must not throw generic exceptions (`java.lang.Exception`)
 GenericException: androidx.heifwriter.HeifWriter#stop(long):
     Methods must not throw generic exceptions (`java.lang.Exception`)
 
-
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setGridEnabled(boolean):
-    androidx.heifwriter.HeifWriter does not declare a `isGridEnabled()` method matching method androidx.heifwriter.HeifWriter.Builder.setGridEnabled(boolean)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setHandler(android.os.Handler):
-    androidx.heifwriter.HeifWriter does not declare a `getHandler()` method matching method androidx.heifwriter.HeifWriter.Builder.setHandler(android.os.Handler)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setMaxImages(int):
-    androidx.heifwriter.HeifWriter does not declare a `getMaxImages()` method matching method androidx.heifwriter.HeifWriter.Builder.setMaxImages(int)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setPrimaryIndex(int):
-    androidx.heifwriter.HeifWriter does not declare a `getPrimaryIndex()` method matching method androidx.heifwriter.HeifWriter.Builder.setPrimaryIndex(int)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setQuality(int):
-    androidx.heifwriter.HeifWriter does not declare a `getQuality()` method matching method androidx.heifwriter.HeifWriter.Builder.setQuality(int)
-MissingGetterMatchingBuilder: androidx.heifwriter.HeifWriter.Builder#setRotation(int):
-    androidx.heifwriter.HeifWriter does not declare a `getRotation()` method matching method androidx.heifwriter.HeifWriter.Builder.setRotation(int)
-
-
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#build():
+    Missing nullability on method `build` return
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#setGridEnabled(boolean):
+    Missing nullability on method `setGridEnabled` return
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#setHandler(android.os.Handler):
+    Missing nullability on method `setHandler` return
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#setMaxImages(int):
+    Missing nullability on method `setMaxImages` return
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#setPrimaryIndex(int):
+    Missing nullability on method `setPrimaryIndex` return
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#setQuality(int):
+    Missing nullability on method `setQuality` return
+MissingNullability: androidx.heifwriter.AvifWriter.Builder#setRotation(int):
+    Missing nullability on method `setRotation` return
 MissingNullability: androidx.heifwriter.HeifWriter.Builder#build():
     Missing nullability on method `build` return
 MissingNullability: androidx.heifwriter.HeifWriter.Builder#setGridEnabled(boolean):
@@ -33,10 +34,20 @@
     Missing nullability on method `setRotation` return
 
 
+UseParcelFileDescriptor: androidx.heifwriter.AvifWriter.Builder#Builder(java.io.FileDescriptor, int, int, int) parameter #0:
+    Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in androidx.heifwriter.AvifWriter.Builder(java.io.FileDescriptor fd, int width, int height, int inputMode)
 UseParcelFileDescriptor: androidx.heifwriter.HeifWriter.Builder#Builder(java.io.FileDescriptor, int, int, int) parameter #0:
     Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in androidx.heifwriter.HeifWriter.Builder(java.io.FileDescriptor fd, int width, int height, int inputMode)
 
 
+VisiblySynchronized: androidx.heifwriter.AvifWriter#addBitmap(android.graphics.Bitmap):
+    Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.addBitmap(android.graphics.Bitmap)
+VisiblySynchronized: androidx.heifwriter.AvifWriter#addYuvBuffer(int, byte[]):
+    Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.addYuvBuffer(int,byte[])
+VisiblySynchronized: androidx.heifwriter.AvifWriter#setInputEndOfStreamTimestamp(long):
+    Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.setInputEndOfStreamTimestamp(long)
+VisiblySynchronized: androidx.heifwriter.AvifWriter#stop(long):
+    Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.AvifWriter.stop(long)
 VisiblySynchronized: androidx.heifwriter.HeifWriter#addBitmap(android.graphics.Bitmap):
     Internal locks must not be exposed (synchronizing on this or class is still externally observable): method androidx.heifwriter.HeifWriter.addBitmap(android.graphics.Bitmap)
 VisiblySynchronized: androidx.heifwriter.HeifWriter#addYuvBuffer(int, byte[]):
diff --git a/heifwriter/heifwriter/api/current.txt b/heifwriter/heifwriter/api/current.txt
index 8a45d85..7ac9fc9 100644
--- a/heifwriter/heifwriter/api/current.txt
+++ b/heifwriter/heifwriter/api/current.txt
@@ -1,30 +1,71 @@
 // Signature format: 4.0
 package androidx.heifwriter {
 
+  public final class AvifWriter implements java.lang.AutoCloseable {
+    method public void addBitmap(android.graphics.Bitmap);
+    method public void addExifData(int, byte[], int, int);
+    method public void addYuvBuffer(int, byte[]);
+    method public void close();
+    method public android.os.Handler? getHandler();
+    method public android.view.Surface getInputSurface();
+    method public int getMaxImages();
+    method public int getPrimaryIndex();
+    method public int getQuality();
+    method public int getRotation();
+    method public boolean isGridEnabled();
+    method public boolean isHighBitDepthEnabled();
+    method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
+    method public void start();
+    method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
+    field public static final int INPUT_MODE_BITMAP = 2; // 0x2
+    field public static final int INPUT_MODE_BUFFER = 0; // 0x0
+    field public static final int INPUT_MODE_SURFACE = 1; // 0x1
+  }
+
+  public static final class AvifWriter.Builder {
+    ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+    method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
+    method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
+    method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
+    method public androidx.heifwriter.AvifWriter.Builder setMaxImages(@IntRange(from=1) int);
+    method public androidx.heifwriter.AvifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+    method public androidx.heifwriter.AvifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+    method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
+  }
+
   public final class HeifWriter implements java.lang.AutoCloseable {
     method public void addBitmap(android.graphics.Bitmap);
     method public void addExifData(int, byte[], int, int);
     method public void addYuvBuffer(int, byte[]);
     method public void close();
+    method public android.os.Handler? getHandler();
     method public android.view.Surface getInputSurface();
-    method public void setInputEndOfStreamTimestamp(long);
+    method public int getMaxImages();
+    method public int getPrimaryIndex();
+    method public int getQuality();
+    method public int getRotation();
+    method public boolean isGridEnabled();
+    method public boolean isHighBitDepthEnabled();
+    method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
     method public void start();
-    method public void stop(long) throws java.lang.Exception;
+    method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
     field public static final int INPUT_MODE_BITMAP = 2; // 0x2
     field public static final int INPUT_MODE_BUFFER = 0; // 0x0
     field public static final int INPUT_MODE_SURFACE = 1; // 0x1
   }
 
   public static final class HeifWriter.Builder {
-    ctor public HeifWriter.Builder(String, int, int, int);
-    ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+    ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
     method public androidx.heifwriter.HeifWriter! build() throws java.io.IOException;
     method public androidx.heifwriter.HeifWriter.Builder! setGridEnabled(boolean);
     method public androidx.heifwriter.HeifWriter.Builder! setHandler(android.os.Handler?);
-    method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setQuality(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setRotation(int);
+    method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(@IntRange(from=1) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(@IntRange(from=0) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setQuality(@IntRange(from=0, to=100) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setRotation(@IntRange(from=0) int);
   }
 
 }
diff --git a/heifwriter/heifwriter/api/public_plus_experimental_current.txt b/heifwriter/heifwriter/api/public_plus_experimental_current.txt
index 8a45d85..7ac9fc9 100644
--- a/heifwriter/heifwriter/api/public_plus_experimental_current.txt
+++ b/heifwriter/heifwriter/api/public_plus_experimental_current.txt
@@ -1,30 +1,71 @@
 // Signature format: 4.0
 package androidx.heifwriter {
 
+  public final class AvifWriter implements java.lang.AutoCloseable {
+    method public void addBitmap(android.graphics.Bitmap);
+    method public void addExifData(int, byte[], int, int);
+    method public void addYuvBuffer(int, byte[]);
+    method public void close();
+    method public android.os.Handler? getHandler();
+    method public android.view.Surface getInputSurface();
+    method public int getMaxImages();
+    method public int getPrimaryIndex();
+    method public int getQuality();
+    method public int getRotation();
+    method public boolean isGridEnabled();
+    method public boolean isHighBitDepthEnabled();
+    method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
+    method public void start();
+    method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
+    field public static final int INPUT_MODE_BITMAP = 2; // 0x2
+    field public static final int INPUT_MODE_BUFFER = 0; // 0x0
+    field public static final int INPUT_MODE_SURFACE = 1; // 0x1
+  }
+
+  public static final class AvifWriter.Builder {
+    ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+    method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
+    method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
+    method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
+    method public androidx.heifwriter.AvifWriter.Builder setMaxImages(@IntRange(from=1) int);
+    method public androidx.heifwriter.AvifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+    method public androidx.heifwriter.AvifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+    method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
+  }
+
   public final class HeifWriter implements java.lang.AutoCloseable {
     method public void addBitmap(android.graphics.Bitmap);
     method public void addExifData(int, byte[], int, int);
     method public void addYuvBuffer(int, byte[]);
     method public void close();
+    method public android.os.Handler? getHandler();
     method public android.view.Surface getInputSurface();
-    method public void setInputEndOfStreamTimestamp(long);
+    method public int getMaxImages();
+    method public int getPrimaryIndex();
+    method public int getQuality();
+    method public int getRotation();
+    method public boolean isGridEnabled();
+    method public boolean isHighBitDepthEnabled();
+    method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
     method public void start();
-    method public void stop(long) throws java.lang.Exception;
+    method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
     field public static final int INPUT_MODE_BITMAP = 2; // 0x2
     field public static final int INPUT_MODE_BUFFER = 0; // 0x0
     field public static final int INPUT_MODE_SURFACE = 1; // 0x1
   }
 
   public static final class HeifWriter.Builder {
-    ctor public HeifWriter.Builder(String, int, int, int);
-    ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+    ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
     method public androidx.heifwriter.HeifWriter! build() throws java.io.IOException;
     method public androidx.heifwriter.HeifWriter.Builder! setGridEnabled(boolean);
     method public androidx.heifwriter.HeifWriter.Builder! setHandler(android.os.Handler?);
-    method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setQuality(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setRotation(int);
+    method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(@IntRange(from=1) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(@IntRange(from=0) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setQuality(@IntRange(from=0, to=100) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setRotation(@IntRange(from=0) int);
   }
 
 }
diff --git a/heifwriter/heifwriter/api/restricted_current.txt b/heifwriter/heifwriter/api/restricted_current.txt
index 8a45d85..7ac9fc9 100644
--- a/heifwriter/heifwriter/api/restricted_current.txt
+++ b/heifwriter/heifwriter/api/restricted_current.txt
@@ -1,30 +1,71 @@
 // Signature format: 4.0
 package androidx.heifwriter {
 
+  public final class AvifWriter implements java.lang.AutoCloseable {
+    method public void addBitmap(android.graphics.Bitmap);
+    method public void addExifData(int, byte[], int, int);
+    method public void addYuvBuffer(int, byte[]);
+    method public void close();
+    method public android.os.Handler? getHandler();
+    method public android.view.Surface getInputSurface();
+    method public int getMaxImages();
+    method public int getPrimaryIndex();
+    method public int getQuality();
+    method public int getRotation();
+    method public boolean isGridEnabled();
+    method public boolean isHighBitDepthEnabled();
+    method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
+    method public void start();
+    method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
+    field public static final int INPUT_MODE_BITMAP = 2; // 0x2
+    field public static final int INPUT_MODE_BUFFER = 0; // 0x0
+    field public static final int INPUT_MODE_SURFACE = 1; // 0x1
+  }
+
+  public static final class AvifWriter.Builder {
+    ctor public AvifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    ctor public AvifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    method public androidx.heifwriter.AvifWriter build() throws java.io.IOException;
+    method public androidx.heifwriter.AvifWriter.Builder setGridEnabled(boolean);
+    method public androidx.heifwriter.AvifWriter.Builder setHandler(android.os.Handler?);
+    method public androidx.heifwriter.AvifWriter.Builder setHighBitDepthEnabled(boolean);
+    method public androidx.heifwriter.AvifWriter.Builder setMaxImages(@IntRange(from=1) int);
+    method public androidx.heifwriter.AvifWriter.Builder setPrimaryIndex(@IntRange(from=0) int);
+    method public androidx.heifwriter.AvifWriter.Builder setQuality(@IntRange(from=0, to=100) int);
+    method public androidx.heifwriter.AvifWriter.Builder setRotation(@IntRange(from=0) int);
+  }
+
   public final class HeifWriter implements java.lang.AutoCloseable {
     method public void addBitmap(android.graphics.Bitmap);
     method public void addExifData(int, byte[], int, int);
     method public void addYuvBuffer(int, byte[]);
     method public void close();
+    method public android.os.Handler? getHandler();
     method public android.view.Surface getInputSurface();
-    method public void setInputEndOfStreamTimestamp(long);
+    method public int getMaxImages();
+    method public int getPrimaryIndex();
+    method public int getQuality();
+    method public int getRotation();
+    method public boolean isGridEnabled();
+    method public boolean isHighBitDepthEnabled();
+    method public void setInputEndOfStreamTimestamp(@IntRange(from=0) long);
     method public void start();
-    method public void stop(long) throws java.lang.Exception;
+    method public void stop(@IntRange(from=0) long) throws java.lang.Exception;
     field public static final int INPUT_MODE_BITMAP = 2; // 0x2
     field public static final int INPUT_MODE_BUFFER = 0; // 0x0
     field public static final int INPUT_MODE_SURFACE = 1; // 0x1
   }
 
   public static final class HeifWriter.Builder {
-    ctor public HeifWriter.Builder(String, int, int, int);
-    ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+    ctor public HeifWriter.Builder(String, @IntRange(from=1) int, @IntRange(from=1) int, int);
+    ctor public HeifWriter.Builder(java.io.FileDescriptor, @IntRange(from=1) int, @IntRange(from=1) int, int);
     method public androidx.heifwriter.HeifWriter! build() throws java.io.IOException;
     method public androidx.heifwriter.HeifWriter.Builder! setGridEnabled(boolean);
     method public androidx.heifwriter.HeifWriter.Builder! setHandler(android.os.Handler?);
-    method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setQuality(int);
-    method public androidx.heifwriter.HeifWriter.Builder! setRotation(int);
+    method public androidx.heifwriter.HeifWriter.Builder! setMaxImages(@IntRange(from=1) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setPrimaryIndex(@IntRange(from=0) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setQuality(@IntRange(from=0, to=100) int);
+    method public androidx.heifwriter.HeifWriter.Builder! setRotation(@IntRange(from=0) int);
   }
 
 }
diff --git a/heifwriter/heifwriter/lint-baseline.xml b/heifwriter/heifwriter/lint-baseline.xml
index 3a578ac..b2f2806 100644
--- a/heifwriter/heifwriter/lint-baseline.xml
+++ b/heifwriter/heifwriter/lint-baseline.xml
@@ -7,7 +7,7 @@
         errorLine1="        synchronized void updateInputEOSTime(long timestampNs) {"
         errorLine2="        ^">
         <location
-            file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+            file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
     </issue>
 
     <issue
@@ -16,7 +16,7 @@
         errorLine1="        synchronized boolean updateLastInputAndEncoderTime(long inputTimeNs, long encoderTimeUs) {"
         errorLine2="        ^">
         <location
-            file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+            file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
     </issue>
 
     <issue
@@ -25,7 +25,7 @@
         errorLine1="        synchronized void updateLastOutputTime(long outputTimeUs) {"
         errorLine2="        ^">
         <location
-            file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+            file="src/main/java/androidx/heifwriter/EncoderBase.java"/>
     </issue>
 
     <issue
@@ -34,7 +34,7 @@
         errorLine1="        synchronized void waitForResult(long timeoutMs) throws Exception {"
         errorLine2="        ^">
         <location
-            file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
+            file="src/main/java/androidx/heifwriter/WriterBase.java"/>
     </issue>
 
     <issue
@@ -43,7 +43,7 @@
         errorLine1="        synchronized void signalResult(@Nullable Exception e) {"
         errorLine2="        ^">
         <location
-            file="src/main/java/androidx/heifwriter/HeifWriter.java"/>
+            file="src/main/java/androidx/heifwriter/WriterBase.java"/>
     </issue>
 
     <issue
@@ -121,6 +121,24 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        protected static String findHevcFallback() {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/HeifEncoder.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        protected static String findAv1Fallback() {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifEncoder.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="        public Builder setRotation(int rotation) {"
         errorLine2="               ~~~~~~~">
         <location
@@ -184,6 +202,69 @@
     <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public Builder setRotation(int rotation) {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public Builder setGridEnabled(boolean gridEnabled) {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public Builder setQuality(int quality) {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public Builder setMaxImages(int maxImages) {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public Builder setPrimaryIndex(int primaryIndex) {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public Builder setHandler(@Nullable Handler handler) {"
+        errorLine2="               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
+        errorLine1="        public HeifWriter build() throws IOException {"
+        errorLine2="               ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/heifwriter/AvifWriter.java"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public void loadTexture(int texId, Bitmap bitmap) {"
         errorLine2="                                       ~~~~~~">
         <location
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/AvifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/AvifWriterTest.java
new file mode 100644
index 0000000..5aadb65
--- /dev/null
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/AvifWriterTest.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.heifwriter;
+
+import static androidx.heifwriter.AvifWriter.INPUT_MODE_BITMAP;
+import static androidx.heifwriter.AvifWriter.INPUT_MODE_BUFFER;
+import static androidx.heifwriter.AvifWriter.INPUT_MODE_SURFACE;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import android.Manifest;
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+
+import androidx.heifwriter.test.R;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Test {@link AvifWriter}.
+ */
+@RunWith(AndroidJUnit4.class)
+@FlakyTest
+public class AvifWriterTest extends TestBase {
+    private static final String TAG = AvifWriterTest.class.getSimpleName();
+
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule1 =
+        GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
+
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+
+    private static final boolean DEBUG = true;
+    private static final boolean DUMP_YUV_INPUT = false;
+
+    private static final String AVIFWRITER_INPUT = "heifwriter_input.heic";
+    private static final int[] IMAGE_RESOURCES = new int[] {
+        R.raw.heifwriter_input
+    };
+    private static final String[] IMAGE_FILENAMES = new String[] {
+        AVIFWRITER_INPUT
+    };
+    private static final String OUTPUT_FILENAME = "output.avif";
+
+    private EglWindowSurface mInputEglSurface;
+    private Handler mHandler;
+    private int mInputIndex;
+
+    @Before
+    public void setUp() throws Exception {
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
+                IMAGE_FILENAMES[i]).getAbsolutePath();
+
+            InputStream inputStream = null;
+            FileOutputStream outputStream = null;
+            try {
+                inputStream = getApplicationContext()
+                    .getResources().openRawResource(IMAGE_RESOURCES[i]);
+                outputStream = new FileOutputStream(outputPath);
+                copy(inputStream, outputStream);
+            } finally {
+                closeQuietly(inputStream);
+                closeQuietly(outputStream);
+            }
+        }
+
+        HandlerThread handlerThread = new HandlerThread(
+            "AvifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String imageFilePath = new File(getApplicationContext().getExternalFilesDir(null),
+                IMAGE_FILENAMES[i]).getAbsolutePath();
+            File imageFile = new File(imageFilePath);
+            if (imageFile.exists()) {
+                imageFile.delete();
+            }
+        }
+    }
+
+    @Test
+    @LargeTest
+    public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, false, false, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+    @Test
+    @LargeTest
+    public void testInputBuffer_Grid_NoHandler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, true, false, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+    @Test
+    @LargeTest
+    public void testInputBuffer_NoGrid_Handler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, false, true, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+    @Test
+    @LargeTest
+    public void testInputBuffer_Grid_Handler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, true, true, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputSurface_NoGrid_NoHandler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, false, false, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+    //
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputSurface_Grid_NoHandler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, true, false, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputSurface_NoGrid_Handler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, false, true, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputSurface_Grid_Handler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, true, true, OUTPUT_FILENAME);
+        doTestForVariousNumberImages(builder);
+    }
+
+
+    @Test
+    @LargeTest
+    public void testInputBitmap_NoGrid_NoHandler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, false, false, OUTPUT_FILENAME);
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+                IMAGE_FILENAMES[i]).getAbsolutePath();
+            doTestForVariousNumberImages(builder.setInputPath(inputPath));
+        }
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputBitmap_Grid_NoHandler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, true, false, OUTPUT_FILENAME);
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+                IMAGE_FILENAMES[i]).getAbsolutePath();
+            doTestForVariousNumberImages(builder.setInputPath(inputPath));
+        }
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputBitmap_NoGrid_Handler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, false, true, OUTPUT_FILENAME);
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+                IMAGE_FILENAMES[i]).getAbsolutePath();
+            doTestForVariousNumberImages(builder.setInputPath(inputPath));
+        }
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @LargeTest
+    public void testInputBitmap_Grid_Handler() throws Throwable {
+        if (shouldSkip()) return;
+
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, true, true, OUTPUT_FILENAME);
+        for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
+            String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
+                IMAGE_FILENAMES[i]).getAbsolutePath();
+            doTestForVariousNumberImages(builder.setInputPath(inputPath));
+        }
+    }
+
+    @SdkSuppress(maxSdkVersion = 29) // b/192261638
+    @Test
+    @SmallTest
+    public void testCloseWithoutStart() throws Throwable {
+        if (shouldSkip()) return;
+
+        final String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
+            OUTPUT_FILENAME).getAbsolutePath();
+        AvifWriter avifWriter = new AvifWriter.Builder(
+            outputPath, 1920, 1080, INPUT_MODE_SURFACE)
+            .setGridEnabled(true)
+            .setMaxImages(4)
+            .setQuality(90)
+            .setPrimaryIndex(0)
+            .setHandler(mHandler)
+            .build();
+
+        avifWriter.close();
+    }
+
+    private void drawFrame(int width, int height) {
+        mInputEglSurface.makeCurrent();
+        generateSurfaceFrame(mInputIndex, width, height);
+        mInputEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex));
+        mInputEglSurface.swapBuffers();
+        mInputIndex++;
+    }
+
+    private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
+        builder.setNumImages(4);
+        doTest(builder.setRotation(270).build());
+        doTest(builder.setRotation(180).build());
+        doTest(builder.setRotation(90).build());
+        doTest(builder.setRotation(0).build());
+        doTest(builder.setNumImages(1).build());
+        doTest(builder.setNumImages(8).build());
+    }
+
+    private boolean shouldSkip() {
+        return !hasEncoderForMime(MediaFormat.MIMETYPE_VIDEO_AV1);
+    }
+
+    private static byte[] mYuvData;
+    private void doTest(final TestConfig config) throws Exception {
+        final int width = config.mWidth;
+        final int height = config.mHeight;
+        final int actualNumImages = config.mActualNumImages;
+
+        mInputIndex = 0;
+        AvifWriter avifWriter = null;
+        FileInputStream inputStream = null;
+        FileOutputStream outputStream = null;
+        try {
+            if (DEBUG)
+                Log.d(TAG, "started: " + config);
+
+            avifWriter = new AvifWriter.Builder(
+                new File(getApplicationContext().getExternalFilesDir(null),
+                    OUTPUT_FILENAME).getAbsolutePath(), width, height, config.mInputMode)
+                .setRotation(config.mRotation)
+                .setGridEnabled(config.mUseGrid)
+                .setMaxImages(config.mMaxNumImages)
+                .setQuality(config.mQuality)
+                .setPrimaryIndex(config.mMaxNumImages - 1)
+                .setHandler(config.mUseHandler ? mHandler : null)
+                .build();
+
+            if (config.mInputMode == INPUT_MODE_SURFACE) {
+                mInputEglSurface = new EglWindowSurface(avifWriter.getInputSurface());
+            }
+
+            avifWriter.start();
+
+            if (config.mInputMode == INPUT_MODE_BUFFER) {
+                if (mYuvData == null || mYuvData.length != width * height * 3 / 2) {
+                    mYuvData = new byte[width * height * 3 / 2];
+                }
+
+                if (config.mInputPath != null) {
+                    inputStream = new FileInputStream(config.mInputPath);
+                }
+
+                if (DUMP_YUV_INPUT) {
+                    File outputFile = new File("/sdcard/input.yuv");
+                    outputFile.createNewFile();
+                    outputStream = new FileOutputStream(outputFile);
+                }
+
+                for (int i = 0; i < actualNumImages; i++) {
+                    if (DEBUG)
+                        Log.d(TAG, "fillYuvBuffer: " + i);
+                    fillYuvBuffer(i, mYuvData, width, height, inputStream);
+                    if (DUMP_YUV_INPUT) {
+                        Log.d(TAG, "@@@ dumping input YUV");
+                        outputStream.write(mYuvData);
+                    }
+                    avifWriter.addYuvBuffer(ImageFormat.YUV_420_888, mYuvData);
+                }
+            } else if (config.mInputMode == INPUT_MODE_SURFACE) {
+                // The input surface is a surface texture using single buffer mode, draws will be
+                // blocked until onFrameAvailable is done with the buffer, which is dependant on
+                // how fast MediaCodec processes them, which is further dependent on how fast the
+                // MediaCodec callbacks are handled. We can't put draws on the same looper that
+                // handles MediaCodec callback, it will cause deadlock.
+                for (int i = 0; i < actualNumImages; i++) {
+                    if (DEBUG)
+                        Log.d(TAG, "drawFrame: " + i);
+                    drawFrame(width, height);
+                }
+                avifWriter.setInputEndOfStreamTimestamp(
+                    1000 * computePresentationTime(actualNumImages - 1));
+            } else if (config.mInputMode == INPUT_MODE_BITMAP) {
+                Bitmap[] bitmaps = config.mBitmaps;
+                for (int i = 0; i < Math.min(bitmaps.length, actualNumImages); i++) {
+                    if (DEBUG)
+                        Log.d(TAG, "addBitmap: " + i);
+                    avifWriter.addBitmap(bitmaps[i]);
+                    bitmaps[i].recycle();
+                }
+            }
+
+            avifWriter.stop(10000);
+            // The test sets the primary index to the last image.
+            // However, if we're testing early abort, the last image will not be
+            // present and the muxer is supposed to set it to 0 by default.
+            int expectedPrimary = config.mMaxNumImages - 1;
+            int expectedImageCount = config.mMaxNumImages;
+            if (actualNumImages < config.mMaxNumImages) {
+                expectedPrimary = 0;
+                expectedImageCount = actualNumImages;
+            }
+            verifyResult(config.mOutputPath, width, height, config.mRotation,
+                expectedImageCount, expectedPrimary, config.mUseGrid,
+                config.mInputMode == INPUT_MODE_SURFACE);
+            if (DEBUG)
+                Log.d(TAG, "finished: PASS");
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+            } catch (IOException e) {
+            }
+
+            if (avifWriter != null) {
+                avifWriter.close();
+                avifWriter = null;
+            }
+            if (mInputEglSurface != null) {
+                // This also releases the surface from encoder.
+                mInputEglSurface.release();
+                mInputEglSurface = null;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index b8e3752..f1f65ab 100644
--- a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -21,28 +21,15 @@
 import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
 import android.Manifest;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
 import android.graphics.ImageFormat;
-import android.graphics.Rect;
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecList;
-import android.media.MediaExtractor;
 import android.media.MediaFormat;
-import android.media.MediaMetadataRetriever;
-import android.opengl.GLES20;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
 import android.util.Log;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.heifwriter.test.R;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
@@ -58,62 +45,38 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Arrays;
 
 /**
  * Test {@link HeifWriter}.
  */
 @RunWith(AndroidJUnit4.class)
 @FlakyTest
-public class HeifWriterTest {
+public class HeifWriterTest extends TestBase {
     private static final String TAG = HeifWriterTest.class.getSimpleName();
 
-    private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-
     @Rule
     public GrantPermissionRule mRuntimePermissionRule1 =
-            GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
+        GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
 
     @Rule
     public GrantPermissionRule mRuntimePermissionRule =
-            GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+        GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
 
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
     private static final boolean DUMP_YUV_INPUT = false;
 
-    private static final byte[][] TEST_YUV_COLORS = {
-            {(byte) 255, (byte) 0, (byte) 0},
-            {(byte) 255, (byte) 0, (byte) 255},
-            {(byte) 255, (byte) 255, (byte) 255},
-            {(byte) 255, (byte) 255, (byte) 0},
-    };
-    private static final Color COLOR_BLOCK =
-            Color.valueOf(1.0f, 1.0f, 1.0f);
-    private static final Color[] COLOR_BARS = {
-            Color.valueOf(0.0f, 0.0f, 0.0f),
-            Color.valueOf(0.0f, 0.0f, 0.64f),
-            Color.valueOf(0.0f, 0.64f, 0.0f),
-            Color.valueOf(0.0f, 0.64f, 0.64f),
-            Color.valueOf(0.64f, 0.0f, 0.0f),
-            Color.valueOf(0.64f, 0.0f, 0.64f),
-            Color.valueOf(0.64f, 0.64f, 0.0f),
-    };
-    private static final float MAX_DELTA = 0.025f;
-    private static final int BORDER_WIDTH = 16;
-
     private static final String HEIFWRITER_INPUT = "heifwriter_input.heic";
     private static final int[] IMAGE_RESOURCES = new int[] {
-            R.raw.heifwriter_input
+        R.raw.heifwriter_input
     };
     private static final String[] IMAGE_FILENAMES = new String[] {
-            HEIFWRITER_INPUT
+        HEIFWRITER_INPUT
     };
     private static final String OUTPUT_FILENAME = "output.heic";
 
@@ -125,13 +88,13 @@
     public void setUp() throws Exception {
         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
             String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                    IMAGE_FILENAMES[i]).getAbsolutePath();
+                IMAGE_FILENAMES[i]).getAbsolutePath();
 
             InputStream inputStream = null;
             FileOutputStream outputStream = null;
             try {
                 inputStream = getApplicationContext()
-                        .getResources().openRawResource(IMAGE_RESOURCES[i]);
+                    .getResources().openRawResource(IMAGE_RESOURCES[i]);
                 outputStream = new FileOutputStream(outputPath);
                 copy(inputStream, outputStream);
             } finally {
@@ -141,7 +104,7 @@
         }
 
         HandlerThread handlerThread = new HandlerThread(
-                "HeifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
+            "HeifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
         handlerThread.start();
         mHandler = new Handler(handlerThread.getLooper());
     }
@@ -150,7 +113,7 @@
     public void tearDown() throws Exception {
         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
             String imageFilePath = new File(getApplicationContext().getExternalFilesDir(null),
-                    IMAGE_FILENAMES[i]).getAbsolutePath();
+                IMAGE_FILENAMES[i]).getAbsolutePath();
             File imageFile = new File(imageFilePath);
             if (imageFile.exists()) {
                 imageFile.delete();
@@ -164,7 +127,8 @@
     public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, false, false);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, false, false, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
@@ -174,7 +138,8 @@
     public void testInputBuffer_Grid_NoHandler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, true, false);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, true, false, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
@@ -184,7 +149,8 @@
     public void testInputBuffer_NoGrid_Handler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, false, true);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, false, true, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
@@ -194,7 +160,8 @@
     public void testInputBuffer_Grid_Handler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, true, true);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BUFFER, true, true, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
@@ -204,17 +171,19 @@
     public void testInputSurface_NoGrid_NoHandler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, false, false);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, false, false, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
-
+    //
     @SdkSuppress(maxSdkVersion = 29) // b/192261638
     @Test
     @LargeTest
     public void testInputSurface_Grid_NoHandler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, true, false);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, true, false, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
@@ -224,7 +193,8 @@
     public void testInputSurface_NoGrid_Handler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, false, true);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, false, true, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
@@ -234,20 +204,23 @@
     public void testInputSurface_Grid_Handler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, true, true);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_SURFACE, true, true, OUTPUT_FILENAME);
         doTestForVariousNumberImages(builder);
     }
 
+
     @SdkSuppress(maxSdkVersion = 29) // b/192261638
     @Test
     @LargeTest
     public void testInputBitmap_NoGrid_NoHandler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, false, false);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, false, false, OUTPUT_FILENAME);
         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
             String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                    IMAGE_FILENAMES[i]).getAbsolutePath();
+                IMAGE_FILENAMES[i]).getAbsolutePath();
             doTestForVariousNumberImages(builder.setInputPath(inputPath));
         }
     }
@@ -258,10 +231,11 @@
     public void testInputBitmap_Grid_NoHandler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, true, false);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, true, false, OUTPUT_FILENAME);
         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
             String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                    IMAGE_FILENAMES[i]).getAbsolutePath();
+                IMAGE_FILENAMES[i]).getAbsolutePath();
             doTestForVariousNumberImages(builder.setInputPath(inputPath));
         }
     }
@@ -272,10 +246,11 @@
     public void testInputBitmap_NoGrid_Handler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, false, true);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, false, true, OUTPUT_FILENAME);
         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
             String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                    IMAGE_FILENAMES[i]).getAbsolutePath();
+                IMAGE_FILENAMES[i]).getAbsolutePath();
             doTestForVariousNumberImages(builder.setInputPath(inputPath));
         }
     }
@@ -286,10 +261,11 @@
     public void testInputBitmap_Grid_Handler() throws Throwable {
         if (shouldSkip()) return;
 
-        TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, true, true);
+        TestConfig.Builder builder =
+            new TestConfig.Builder(INPUT_MODE_BITMAP, true, true, OUTPUT_FILENAME);
         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
             String inputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                    IMAGE_FILENAMES[i]).getAbsolutePath();
+                IMAGE_FILENAMES[i]).getAbsolutePath();
             doTestForVariousNumberImages(builder.setInputPath(inputPath));
         }
     }
@@ -301,19 +277,27 @@
         if (shouldSkip()) return;
 
         final String outputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                        OUTPUT_FILENAME).getAbsolutePath();
+            OUTPUT_FILENAME).getAbsolutePath();
         HeifWriter heifWriter = new HeifWriter.Builder(
-                    outputPath, 1920, 1080, INPUT_MODE_SURFACE)
-                    .setGridEnabled(true)
-                    .setMaxImages(4)
-                    .setQuality(90)
-                    .setPrimaryIndex(0)
-                    .setHandler(mHandler)
-                    .build();
+            outputPath, 1920, 1080, INPUT_MODE_SURFACE)
+            .setGridEnabled(true)
+            .setMaxImages(4)
+            .setQuality(90)
+            .setPrimaryIndex(0)
+            .setHandler(mHandler)
+            .build();
 
         heifWriter.close();
     }
 
+    private void drawFrame(int width, int height) {
+        mInputEglSurface.makeCurrent();
+        generateSurfaceFrame(mInputIndex, width, height);
+        mInputEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex));
+        mInputEglSurface.swapBuffers();
+        mInputIndex++;
+    }
+
     private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
         builder.setNumImages(4);
         doTest(builder.setRotation(270).build());
@@ -324,186 +308,11 @@
         doTest(builder.setNumImages(8).build());
     }
 
-    private void closeQuietly(Closeable closeable) {
-        if (closeable != null) {
-            try {
-                closeable.close();
-            } catch (RuntimeException rethrown) {
-                throw rethrown;
-            } catch (Exception ignored) {
-            }
-        }
-    }
-
-    private int copy(InputStream in, OutputStream out) throws IOException {
-        int total = 0;
-        byte[] buffer = new byte[8192];
-        int c;
-        while ((c = in.read(buffer)) != -1) {
-            total += c;
-            out.write(buffer, 0, c);
-        }
-        return total;
-    }
-
     private boolean shouldSkip() {
         return !hasEncoderForMime(MediaFormat.MIMETYPE_VIDEO_HEVC)
             && !hasEncoderForMime(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
     }
 
-    private boolean hasEncoderForMime(String mime) {
-        for (MediaCodecInfo info : sMCL.getCodecInfos()) {
-            if (info.isEncoder()) {
-                for (String type : info.getSupportedTypes()) {
-                    if (type.equalsIgnoreCase(mime)) {
-                        Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    private static class TestConfig {
-        final int mInputMode;
-        final boolean mUseGrid;
-        final boolean mUseHandler;
-        final int mMaxNumImages;
-        final int mActualNumImages;
-        final int mWidth;
-        final int mHeight;
-        final int mRotation;
-        final int mQuality;
-        final String mInputPath;
-        final String mOutputPath;
-        final Bitmap[] mBitmaps;
-
-        TestConfig(int inputMode, boolean useGrid, boolean useHandler,
-                   int maxNumImages, int actualNumImages, int width, int height,
-                   int rotation, int quality,
-                   String inputPath, String outputPath, Bitmap[] bitmaps) {
-            mInputMode = inputMode;
-            mUseGrid = useGrid;
-            mUseHandler = useHandler;
-            mMaxNumImages = maxNumImages;
-            mActualNumImages = actualNumImages;
-            mWidth = width;
-            mHeight = height;
-            mRotation = rotation;
-            mQuality = quality;
-            mInputPath = inputPath;
-            mOutputPath = outputPath;
-            mBitmaps = bitmaps;
-        }
-
-        static class Builder {
-            final int mInputMode;
-            final boolean mUseGrid;
-            final boolean mUseHandler;
-            int mMaxNumImages;
-            int mNumImages;
-            int mWidth;
-            int mHeight;
-            int mRotation;
-            final int mQuality;
-            String mInputPath;
-            final String mOutputPath;
-            Bitmap[] mBitmaps;
-            boolean mNumImagesSetExplicitly;
-
-
-            Builder(int inputMode, boolean useGrids, boolean useHandler) {
-                mInputMode = inputMode;
-                mUseGrid = useGrids;
-                mUseHandler = useHandler;
-                mMaxNumImages = mNumImages = 4;
-                mWidth = 1920;
-                mHeight = 1080;
-                mRotation = 0;
-                mQuality = 100;
-                mOutputPath = new File(getApplicationContext().getExternalFilesDir(null),
-                        OUTPUT_FILENAME).getAbsolutePath();
-            }
-
-            Builder setInputPath(String inputPath) {
-                mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
-                return this;
-            }
-
-            Builder setNumImages(int numImages) {
-                mNumImagesSetExplicitly = true;
-                mNumImages = numImages;
-                return this;
-            }
-
-            Builder setRotation(int rotation) {
-                mRotation = rotation;
-                return this;
-            }
-
-            private void loadBitmapInputs() {
-                if (mInputMode != INPUT_MODE_BITMAP) {
-                    return;
-                }
-                MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-                retriever.setDataSource(mInputPath);
-                String hasImage = retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
-                if (!"yes".equals(hasImage)) {
-                    throw new IllegalArgumentException("no bitmap found!");
-                }
-                mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
-                if (!mNumImagesSetExplicitly) {
-                    mNumImages = mMaxNumImages;
-                }
-                mBitmaps = new Bitmap[mMaxNumImages];
-                for (int i = 0; i < mBitmaps.length; i++) {
-                    mBitmaps[i] = retriever.getImageAtIndex(i);
-                }
-                mWidth = mBitmaps[0].getWidth();
-                mHeight = mBitmaps[0].getHeight();
-                try {
-                    retriever.release();
-                } catch (IOException e) {
-                    // Nothing we can  do about it.
-                }
-            }
-
-            private void cleanupStaleOutputs() {
-                File outputFile = new File(mOutputPath);
-                if (outputFile.exists()) {
-                    outputFile.delete();
-                }
-            }
-
-            TestConfig build() {
-                cleanupStaleOutputs();
-                loadBitmapInputs();
-
-                return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
-                        mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return "TestConfig"
-                    + ": mInputMode " + mInputMode
-                    + ", mUseGrid " + mUseGrid
-                    + ", mUseHandler " + mUseHandler
-                    + ", mMaxNumImages " + mMaxNumImages
-                    + ", mNumImages " + mActualNumImages
-                    + ", mWidth " + mWidth
-                    + ", mHeight " + mHeight
-                    + ", mRotation " + mRotation
-                    + ", mQuality " + mQuality
-                    + ", mInputPath " + mInputPath
-                    + ", mOutputPath " + mOutputPath;
-        }
-    }
-
     private static byte[] mYuvData;
     private void doTest(final TestConfig config) throws Exception {
         final int width = config.mWidth;
@@ -515,17 +324,19 @@
         FileInputStream inputStream = null;
         FileOutputStream outputStream = null;
         try {
-            if (DEBUG) Log.d(TAG, "started: " + config);
+            if (DEBUG)
+                Log.d(TAG, "started: " + config);
 
             heifWriter = new HeifWriter.Builder(
-                    config.mOutputPath, width, height, config.mInputMode)
-                    .setRotation(config.mRotation)
-                    .setGridEnabled(config.mUseGrid)
-                    .setMaxImages(config.mMaxNumImages)
-                    .setQuality(config.mQuality)
-                    .setPrimaryIndex(config.mMaxNumImages - 1)
-                    .setHandler(config.mUseHandler ? mHandler : null)
-                    .build();
+                new File(getApplicationContext().getExternalFilesDir(null),
+                    OUTPUT_FILENAME).getAbsolutePath(), width, height, config.mInputMode)
+                .setRotation(config.mRotation)
+                .setGridEnabled(config.mUseGrid)
+                .setMaxImages(config.mMaxNumImages)
+                .setQuality(config.mQuality)
+                .setPrimaryIndex(config.mMaxNumImages - 1)
+                .setHandler(config.mUseHandler ? mHandler : null)
+                .build();
 
             if (config.mInputMode == INPUT_MODE_SURFACE) {
                 mInputEglSurface = new EglWindowSurface(heifWriter.getInputSurface());
@@ -549,7 +360,8 @@
                 }
 
                 for (int i = 0; i < actualNumImages; i++) {
-                    if (DEBUG) Log.d(TAG, "fillYuvBuffer: " + i);
+                    if (DEBUG)
+                        Log.d(TAG, "fillYuvBuffer: " + i);
                     fillYuvBuffer(i, mYuvData, width, height, inputStream);
                     if (DUMP_YUV_INPUT) {
                         Log.d(TAG, "@@@ dumping input YUV");
@@ -564,15 +376,17 @@
                 // MediaCodec callbacks are handled. We can't put draws on the same looper that
                 // handles MediaCodec callback, it will cause deadlock.
                 for (int i = 0; i < actualNumImages; i++) {
-                    if (DEBUG) Log.d(TAG, "drawFrame: " + i);
+                    if (DEBUG)
+                        Log.d(TAG, "drawFrame: " + i);
                     drawFrame(width, height);
                 }
                 heifWriter.setInputEndOfStreamTimestamp(
-                        1000 * computePresentationTime(actualNumImages - 1));
+                    1000 * computePresentationTime(actualNumImages - 1));
             } else if (config.mInputMode == INPUT_MODE_BITMAP) {
                 Bitmap[] bitmaps = config.mBitmaps;
                 for (int i = 0; i < Math.min(bitmaps.length, actualNumImages); i++) {
-                    if (DEBUG) Log.d(TAG, "addBitmap: " + i);
+                    if (DEBUG)
+                        Log.d(TAG, "addBitmap: " + i);
                     heifWriter.addBitmap(bitmaps[i]);
                     bitmaps[i].recycle();
                 }
@@ -589,9 +403,10 @@
                 expectedImageCount = actualNumImages;
             }
             verifyResult(config.mOutputPath, width, height, config.mRotation,
-                    expectedImageCount, expectedPrimary, config.mUseGrid,
-                    config.mInputMode == INPUT_MODE_SURFACE);
-            if (DEBUG) Log.d(TAG, "finished: PASS");
+                expectedImageCount, expectedPrimary, config.mUseGrid,
+                config.mInputMode == INPUT_MODE_SURFACE);
+            if (DEBUG)
+                Log.d(TAG, "finished: PASS");
         } finally {
             try {
                 if (outputStream != null) {
@@ -600,7 +415,8 @@
                 if (inputStream != null) {
                     inputStream.close();
                 }
-            } catch (IOException e) {}
+            } catch (IOException e) {
+            }
 
             if (heifWriter != null) {
                 heifWriter.close();
@@ -613,139 +429,4 @@
             }
         }
     }
-
-    private long computePresentationTime(int frameIndex) {
-        return 132 + (long)frameIndex * 1000000;
-    }
-
-    private void fillYuvBuffer(int frameIndex, @NonNull byte[] data, int width, int height,
-                               @Nullable FileInputStream inputStream) throws IOException {
-        if (inputStream != null) {
-            inputStream.read(data);
-        } else {
-            byte[] color = TEST_YUV_COLORS[frameIndex % TEST_YUV_COLORS.length];
-            int sizeY = width * height;
-            Arrays.fill(data, 0, sizeY, color[0]);
-            Arrays.fill(data, sizeY, sizeY * 5 / 4, color[1]);
-            Arrays.fill(data, sizeY * 5 / 4, sizeY * 3 / 2, color[2]);
-        }
-    }
-
-    private void drawFrame(int width, int height) {
-        mInputEglSurface.makeCurrent();
-        generateSurfaceFrame(mInputIndex, width, height);
-        mInputEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex));
-        mInputEglSurface.swapBuffers();
-        mInputIndex++;
-    }
-
-    private static Rect getColorBarRect(int index, int width, int height) {
-        int barWidth = (width - BORDER_WIDTH * 2) / COLOR_BARS.length;
-        return new Rect(BORDER_WIDTH + barWidth * index, BORDER_WIDTH,
-                BORDER_WIDTH + barWidth * (index + 1), height - BORDER_WIDTH);
-    }
-
-    private static Rect getColorBlockRect(int index, int width, int height) {
-        int blockCenterX = (width / 5) * (index % 4 + 1);
-        return new Rect(blockCenterX - width / 10, height / 6,
-                        blockCenterX + width / 10, height / 3);
-    }
-
-    private void generateSurfaceFrame(int frameIndex, int width, int height) {
-        GLES20.glViewport(0, 0, width, height);
-        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
-        GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
-        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
-
-        for (int i = 0; i < COLOR_BARS.length; i++) {
-            Rect r = getColorBarRect(i, width, height);
-
-            GLES20.glScissor(r.left, r.top, r.width(), r.height());
-            final Color color = COLOR_BARS[i];
-            GLES20.glClearColor(color.red(), color.green(), color.blue(), 1.0f);
-            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
-        }
-
-        Rect r = getColorBlockRect(frameIndex, width, height);
-        GLES20.glScissor(r.left, r.top, r.width(), r.height());
-        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
-        r.inset(BORDER_WIDTH, BORDER_WIDTH);
-        GLES20.glScissor(r.left, r.top, r.width(), r.height());
-        GLES20.glClearColor(COLOR_BLOCK.red(), COLOR_BLOCK.green(), COLOR_BLOCK.blue(), 1.0f);
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
-    }
-
-    /**
-     * Determines if two color values are approximately equal.
-     */
-    private static boolean approxEquals(Color expected, Color actual) {
-        return (Math.abs(expected.red() - actual.red()) <= MAX_DELTA)
-            && (Math.abs(expected.green() - actual.green()) <= MAX_DELTA)
-            && (Math.abs(expected.blue() - actual.blue()) <= MAX_DELTA);
-    }
-
-    private void verifyResult(
-            String filename, int width, int height, int rotation,
-            int imageCount, int primary, boolean useGrid, boolean checkColor)
-            throws Exception {
-        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-        retriever.setDataSource(filename);
-        String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
-        if (!"yes".equals(hasImage)) {
-            throw new Exception("No images found in file " + filename);
-        }
-        assertEquals("Wrong width", width,
-                Integer.parseInt(retriever.extractMetadata(
-                    MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
-        assertEquals("Wrong height", height,
-                Integer.parseInt(retriever.extractMetadata(
-                    MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
-        assertEquals("Wrong rotation", rotation,
-                Integer.parseInt(retriever.extractMetadata(
-                    MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
-        assertEquals("Wrong image count", imageCount,
-                Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
-        assertEquals("Wrong primary index", primary,
-                Integer.parseInt(retriever.extractMetadata(
-                        MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
-        try {
-            retriever.release();
-        } catch (IOException e) {
-            // Nothing we can  do about it.
-        }
-
-        if (useGrid) {
-            MediaExtractor extractor = new MediaExtractor();
-            extractor.setDataSource(filename);
-            MediaFormat format = extractor.getTrackFormat(0);
-            int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
-            int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
-            int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
-            int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
-            assertTrue("Wrong tile width or grid cols",
-                    ((width + tileWidth - 1) / tileWidth) == gridCols);
-            assertTrue("Wrong tile height or grid rows",
-                    ((height + tileHeight - 1) / tileHeight) == gridRows);
-            extractor.release();
-        }
-
-        if (checkColor) {
-            Bitmap bitmap = BitmapFactory.decodeFile(filename);
-
-            for (int i = 0; i < COLOR_BARS.length; i++) {
-                Rect r = getColorBarRect(i, width, height);
-                assertTrue("Color bar " + i + " doesn't match", approxEquals(COLOR_BARS[i],
-                        Color.valueOf(bitmap.getPixel(r.centerX(), r.centerY()))));
-            }
-
-            Rect r = getColorBlockRect(primary, width, height);
-            assertTrue("Color block doesn't match", approxEquals(COLOR_BLOCK,
-                    Color.valueOf(bitmap.getPixel(r.centerX(), height - r.centerY()))));
-
-            bitmap.recycle();
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/TestBase.java b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/TestBase.java
new file mode 100644
index 0000000..83938e4
--- /dev/null
+++ b/heifwriter/heifwriter/src/androidTest/java/androidx/heifwriter/TestBase.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.heifwriter;
+
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+/**
+ * Base class holding common utilities for {@link HeifWriterTest} and {@link AvifWriterTest}.
+ */
+public class TestBase {
+    private static final String TAG = HeifWriterTest.class.getSimpleName();
+
+    private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+    private static final byte[][] TEST_YUV_COLORS = {
+        {(byte) 255, (byte) 0, (byte) 0},
+        {(byte) 255, (byte) 0, (byte) 255},
+        {(byte) 255, (byte) 255, (byte) 255},
+        {(byte) 255, (byte) 255, (byte) 0},
+    };
+    private static final Color COLOR_BLOCK =
+        Color.valueOf(1.0f, 1.0f, 1.0f);
+    private static final Color[] COLOR_BARS = {
+        Color.valueOf(0.0f, 0.0f, 0.0f),
+        Color.valueOf(0.0f, 0.0f, 0.64f),
+        Color.valueOf(0.0f, 0.64f, 0.0f),
+        Color.valueOf(0.0f, 0.64f, 0.64f),
+        Color.valueOf(0.64f, 0.0f, 0.0f),
+        Color.valueOf(0.64f, 0.0f, 0.64f),
+        Color.valueOf(0.64f, 0.64f, 0.0f),
+    };
+    private static final float MAX_DELTA = 0.025f;
+    private static final int BORDER_WIDTH = 16;
+
+    protected long computePresentationTime(int frameIndex) {
+        return 132 + (long)frameIndex * 1000000;
+    }
+
+    protected void fillYuvBuffer(int frameIndex, @NonNull byte[] data, int width, int height,
+        @Nullable FileInputStream inputStream) throws IOException {
+        if (inputStream != null) {
+            inputStream.read(data);
+        } else {
+            byte[] color = TEST_YUV_COLORS[frameIndex % TEST_YUV_COLORS.length];
+            int sizeY = width * height;
+            Arrays.fill(data, 0, sizeY, color[0]);
+            Arrays.fill(data, sizeY, sizeY * 5 / 4, color[1]);
+            Arrays.fill(data, sizeY * 5 / 4, sizeY * 3 / 2, color[2]);
+        }
+    }
+
+    protected static Rect getColorBarRect(int index, int width, int height) {
+        int barWidth = (width - BORDER_WIDTH * 2) / COLOR_BARS.length;
+        return new Rect(BORDER_WIDTH + barWidth * index, BORDER_WIDTH,
+            BORDER_WIDTH + barWidth * (index + 1), height - BORDER_WIDTH);
+    }
+
+    protected static Rect getColorBlockRect(int index, int width, int height) {
+        int blockCenterX = (width / 5) * (index % 4 + 1);
+        return new Rect(blockCenterX - width / 10, height / 6,
+            blockCenterX + width / 10, height / 3);
+    }
+
+    protected void generateSurfaceFrame(int frameIndex, int width, int height) {
+        GLES20.glViewport(0, 0, width, height);
+        GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+        GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+        GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+
+        for (int i = 0; i < COLOR_BARS.length; i++) {
+            Rect r = getColorBarRect(i, width, height);
+
+            GLES20.glScissor(r.left, r.top, r.width(), r.height());
+            final Color color = COLOR_BARS[i];
+            GLES20.glClearColor(color.red(), color.green(), color.blue(), 1.0f);
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+        }
+
+        Rect r = getColorBlockRect(frameIndex, width, height);
+        GLES20.glScissor(r.left, r.top, r.width(), r.height());
+        GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+        r.inset(BORDER_WIDTH, BORDER_WIDTH);
+        GLES20.glScissor(r.left, r.top, r.width(), r.height());
+        GLES20.glClearColor(COLOR_BLOCK.red(), COLOR_BLOCK.green(), COLOR_BLOCK.blue(), 1.0f);
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+    }
+
+    /**
+     * Determines if two color values are approximately equal.
+     */
+    protected static boolean approxEquals(Color expected, Color actual) {
+        return (Math.abs(expected.red() - actual.red()) <= MAX_DELTA)
+            && (Math.abs(expected.green() - actual.green()) <= MAX_DELTA)
+            && (Math.abs(expected.blue() - actual.blue()) <= MAX_DELTA);
+    }
+
+    protected void verifyResult(
+        String filename, int width, int height, int rotation,
+        int imageCount, int primary, boolean useGrid, boolean checkColor)
+        throws Exception {
+        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+        retriever.setDataSource(filename);
+        String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
+        if (!"yes".equals(hasImage)) {
+            throw new Exception("No images found in file " + filename);
+        }
+        assertEquals("Wrong width", width,
+            Integer.parseInt(retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
+        assertEquals("Wrong height", height,
+            Integer.parseInt(retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+        assertEquals("Wrong rotation", rotation,
+            Integer.parseInt(retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
+        assertEquals("Wrong image count", imageCount,
+            Integer.parseInt(retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+        assertEquals("Wrong primary index", primary,
+            Integer.parseInt(retriever.extractMetadata(
+                MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
+        try {
+            retriever.release();
+        } catch (IOException e) {
+            // Nothing we can  do about it.
+        }
+
+        if (useGrid) {
+            MediaExtractor extractor = new MediaExtractor();
+            extractor.setDataSource(filename);
+            MediaFormat format = extractor.getTrackFormat(0);
+            int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
+            int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
+            int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
+            int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
+            assertTrue("Wrong tile width or grid cols",
+                ((width + tileWidth - 1) / tileWidth) == gridCols);
+            assertTrue("Wrong tile height or grid rows",
+                ((height + tileHeight - 1) / tileHeight) == gridRows);
+            extractor.release();
+        }
+
+        if (checkColor) {
+            Bitmap bitmap = BitmapFactory.decodeFile(filename);
+
+            for (int i = 0; i < COLOR_BARS.length; i++) {
+                Rect r = getColorBarRect(i, width, height);
+                assertTrue("Color bar " + i + " doesn't match", approxEquals(COLOR_BARS[i],
+                    Color.valueOf(bitmap.getPixel(r.centerX(), r.centerY()))));
+            }
+
+            Rect r = getColorBlockRect(primary, width, height);
+            assertTrue("Color block doesn't match", approxEquals(COLOR_BLOCK,
+                Color.valueOf(bitmap.getPixel(r.centerX(), height - r.centerY()))));
+
+            bitmap.recycle();
+        }
+    }
+
+    protected void closeQuietly(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    protected int copy(InputStream in, OutputStream out) throws IOException {
+        int total = 0;
+        byte[] buffer = new byte[8192];
+        int c;
+        while ((c = in.read(buffer)) != -1) {
+            total += c;
+            out.write(buffer, 0, c);
+        }
+        return total;
+    }
+
+    protected boolean hasEncoderForMime(String mime) {
+        for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+            if (info.isEncoder()) {
+                for (String type : info.getSupportedTypes()) {
+                    if (type.equalsIgnoreCase(mime)) {
+                        Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    protected static class TestConfig {
+        final int mInputMode;
+        final boolean mUseGrid;
+        final boolean mUseHandler;
+        final int mMaxNumImages;
+        final int mActualNumImages;
+        final int mWidth;
+        final int mHeight;
+        final int mRotation;
+        final int mQuality;
+        final String mInputPath;
+        final String mOutputPath;
+        final Bitmap[] mBitmaps;
+
+        TestConfig(int inputMode, boolean useGrid, boolean useHandler,
+            int maxNumImages, int actualNumImages, int width, int height,
+            int rotation, int quality,
+            String inputPath, String outputPath, Bitmap[] bitmaps) {
+            mInputMode = inputMode;
+            mUseGrid = useGrid;
+            mUseHandler = useHandler;
+            mMaxNumImages = maxNumImages;
+            mActualNumImages = actualNumImages;
+            mWidth = width;
+            mHeight = height;
+            mRotation = rotation;
+            mQuality = quality;
+            mInputPath = inputPath;
+            mOutputPath = outputPath;
+            mBitmaps = bitmaps;
+        }
+
+        static class Builder {
+            final int mInputMode;
+            final boolean mUseGrid;
+            final boolean mUseHandler;
+            int mMaxNumImages;
+            int mNumImages;
+            int mWidth;
+            int mHeight;
+            int mRotation;
+            final int mQuality;
+            String mInputPath;
+            final String mOutputPath;
+            Bitmap[] mBitmaps;
+            boolean mNumImagesSetExplicitly;
+
+
+            Builder(int inputMode, boolean useGrids, boolean useHandler, String outputFileName) {
+                mInputMode = inputMode;
+                mUseGrid = useGrids;
+                mUseHandler = useHandler;
+                mMaxNumImages = mNumImages = 4;
+                mWidth = 1920;
+                mHeight = 1080;
+                mRotation = 0;
+                mQuality = 100;
+                mOutputPath = new File(getApplicationContext().getExternalFilesDir(null),
+                    outputFileName).getAbsolutePath();
+            }
+
+            Builder setInputPath(String inputPath) {
+                mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
+                return this;
+            }
+
+            Builder setNumImages(int numImages) {
+                mNumImagesSetExplicitly = true;
+                mNumImages = numImages;
+                return this;
+            }
+
+            Builder setRotation(int rotation) {
+                mRotation = rotation;
+                return this;
+            }
+
+            private void loadBitmapInputs() {
+                if (mInputMode != INPUT_MODE_BITMAP) {
+                    return;
+                }
+                MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+                retriever.setDataSource(mInputPath);
+                String hasImage = retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
+                if (!"yes".equals(hasImage)) {
+                    throw new IllegalArgumentException("no bitmap found!");
+                }
+                mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
+                    MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+                if (!mNumImagesSetExplicitly) {
+                    mNumImages = mMaxNumImages;
+                }
+                mBitmaps = new Bitmap[mMaxNumImages];
+                for (int i = 0; i < mBitmaps.length; i++) {
+                    mBitmaps[i] = retriever.getImageAtIndex(i);
+                }
+                mWidth = mBitmaps[0].getWidth();
+                mHeight = mBitmaps[0].getHeight();
+                try {
+                    retriever.release();
+                } catch (IOException e) {
+                    // Nothing we can  do about it.
+                }
+            }
+
+            private void cleanupStaleOutputs() {
+                File outputFile = new File(mOutputPath);
+                if (outputFile.exists()) {
+                    outputFile.delete();
+                }
+            }
+
+            TestConfig build() {
+                cleanupStaleOutputs();
+                loadBitmapInputs();
+
+                return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
+                    mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "TestConfig"
+                + ": mInputMode " + mInputMode
+                + ", mUseGrid " + mUseGrid
+                + ", mUseHandler " + mUseHandler
+                + ", mMaxNumImages " + mMaxNumImages
+                + ", mNumImages " + mActualNumImages
+                + ", mWidth " + mWidth
+                + ", mHeight " + mHeight
+                + ", mRotation " + mRotation
+                + ", mQuality " + mQuality
+                + ", mInputPath " + mInputPath
+                + ", mOutputPath " + mOutputPath;
+        }
+    }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
new file mode 100644
index 0000000..561e0b2
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifEncoder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * This class encodes images into HEIF-compatible samples using AV1 encoder.
+ *
+ * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ *
+ * The output format and samples are sent back in {@link
+ * Callback#onOutputFormatChanged(HeifEncoder, MediaFormat)} and {@link
+ * Callback#onDrainOutputBuffer(HeifEncoder, ByteBuffer)}. If the client
+ * requests to use grid, each tile will be sent back individually.
+ *
+ * HeifEncoder is made a separate class from {@link HeifWriter}, as some more
+ * advanced use cases might want to build solutions on top of the HeifEncoder directly.
+ * (eg. mux still images and video tracks into a single container).
+ *
+ * @hide
+ */
+public final class AvifEncoder extends EncoderBase {
+    private static final String TAG = "AvifEncoder";
+    private static final boolean DEBUG = false;
+
+    protected static final int GRID_WIDTH = 512;
+    protected static final int GRID_HEIGHT = 512;
+    protected static final double MAX_COMPRESS_RATIO = 0.25f;
+
+    private static final MediaCodecList sMCL =
+        new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+    /**
+     * Configure the avif encoding session. Should only be called once.
+     *
+     * @param width Width of the image.
+     * @param height Height of the image.
+     * @param useGrid Whether to encode image into tiles. If enabled, tile size will be
+     *                automatically chosen.
+     * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
+     *                supported by this implementation (which often results in larger file size).
+     * @param inputMode The input type of this encoding session.
+     * @param handler If not null, client will receive all callbacks on the handler's looper.
+     *                Otherwise, client will receive callbacks on a looper created by us.
+     * @param cb The callback to receive various messages from the avif encoder.
+     */
+    public AvifEncoder(int width, int height, boolean useGrid,
+            int quality, @InputMode int inputMode,
+            @Nullable Handler handler, @NonNull Callback cb,
+            boolean useBitDepth10) throws IOException {
+        super("AVIF", width, height, useGrid, quality, inputMode, handler, cb, useBitDepth10);
+        mEncoder.setCallback(new Av1EncoderCallback(), mHandler);
+        finishSettingUpEncoder(useBitDepth10);
+    }
+
+    protected static String findAv1Fallback() {
+        String av1 = null; // first AV1 encoder
+        for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+            if (!info.isEncoder()) {
+                continue;
+            }
+            MediaCodecInfo.CodecCapabilities caps = null;
+            try {
+                caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AV1);
+            } catch (IllegalArgumentException e) { // mime is not supported
+                continue;
+            }
+            if (!caps.getVideoCapabilities().isSizeSupported(GRID_WIDTH, GRID_HEIGHT)) {
+                continue;
+            }
+            if (caps.getEncoderCapabilities().isBitrateModeSupported(
+                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+                // Encoder that supports CQ mode is preferred over others,
+                // return the first encoder that supports CQ mode.
+                // (No need to check if it's hw based, it's already listed in
+                // order of preference.)
+                return info.getName();
+            }
+            if (av1 == null) {
+                av1 = info.getName();
+            }
+        }
+        // If no encoders support CQ, return the first AV1 encoder.
+        return av1;
+    }
+
+    /**
+     * MediaCodec callback for AV1 encoding.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected class Av1EncoderCallback extends EncoderCallback {
+        @Override
+        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
+            if (codec != mEncoder) return;
+
+            if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);
+
+            // TODO(b/252835975) replace "image/avif" with  MIMETYPE_IMAGE_AVIF.
+            if (!format.getString(MediaFormat.KEY_MIME).equals("image/avif")) {
+                format.setString(MediaFormat.KEY_MIME, "image/avif");
+                format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
+                format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
+
+                if (mUseGrid) {
+                    format.setInteger(MediaFormat.KEY_TILE_WIDTH, mGridWidth);
+                    format.setInteger(MediaFormat.KEY_TILE_HEIGHT, mGridHeight);
+                    format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
+                    format.setInteger(MediaFormat.KEY_GRID_COLUMNS, mGridCols);
+                }
+            }
+
+            mCallback.onOutputFormatChanged(AvifEncoder.this, format);
+        }
+    }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
new file mode 100644
index 0000000..706f9dfd
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/AvifWriter.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class writes one or more still images (of the same dimensions) into
+ * an AVIF file.
+ *
+ * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ *
+ * The general sequence (in pseudo-code) to write a avif file using this class is as follows:
+ *
+ * 1) Construct the writer:
+ * AvifWriter avifwriter = new AvifWriter(...);
+ *
+ * 2) If using surface input mode, obtain the input surface:
+ * Surface surface = avifwriter.getInputSurface();
+ *
+ * 3) Call start:
+ * avifwriter.start();
+ *
+ * 4) Depending on the chosen input mode, add one or more images using one of these methods:
+ * avifwriter.addYuvBuffer(...);   Or
+ * avifwriter.addBitmap(...);   Or
+ * render to the previously obtained surface
+ *
+ * 5) Call stop:
+ * avifwriter.stop(...);
+ *
+ * 6) Close the writer:
+ * avifwriter.close();
+ *
+ * Please refer to the documentations on individual methods for the exact usage.
+ */
+@SuppressWarnings("HiddenSuperclass")
+public final class AvifWriter extends WriterBase {
+
+    private static final String TAG = "AvifWriter";
+    private static final boolean DEBUG = false;
+
+    /**
+     * The input mode where the client adds input buffers with YUV data.
+     *
+     * @see #addYuvBuffer(int, byte[])
+     */
+    public static final int INPUT_MODE_BUFFER = WriterBase.INPUT_MODE_BUFFER;
+
+    /**
+     * The input mode where the client renders the images to an input Surface created by the writer.
+     *
+     * The input surface operates in single buffer mode. As a result, for use case where camera
+     * directly outputs to the input surface, this mode will not work because camera framework
+     * requires multiple buffers to operate in a pipeline fashion.
+     *
+     * @see #getInputSurface()
+     */
+    public static final int INPUT_MODE_SURFACE = WriterBase.INPUT_MODE_SURFACE;
+
+    /**
+     * The input mode where the client adds bitmaps.
+     *
+     * @see #addBitmap(Bitmap)
+     */
+    public static final int INPUT_MODE_BITMAP = WriterBase.INPUT_MODE_BITMAP;
+
+    /**
+     * @hide
+     */
+    @IntDef({
+        INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InputMode {
+
+    }
+
+    /**
+     * Builder class for constructing a AvifWriter object from specified parameters.
+     */
+    public static final class Builder {
+        private final String mPath;
+        private final FileDescriptor mFd;
+        private final int mWidth;
+        private final int mHeight;
+        private final @InputMode int mInputMode;
+        private boolean mGridEnabled = true;
+        private int mQuality = 100;
+        private int mMaxImages = 1;
+        private int mPrimaryIndex = 0;
+        private int mRotation = 0;
+        private Handler mHandler;
+        private boolean mHighBitDepthEnabled = false;
+
+        /**
+         * Construct a Builder with output specified by its path.
+         *
+         * @param path Path of the file to be written.
+         * @param width Width of the image in number of pixels.
+         * @param height Height of the image in number of pixels.
+         * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+         *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+         */
+        public Builder(@NonNull String path,
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @InputMode int inputMode) {
+            this(path, null, width, height, inputMode);
+        }
+
+        /**
+         * Construct a Builder with output specified by its file descriptor.
+         *
+         * @param fd File descriptor of the file to be written.
+         * @param width Width of the image in number of pixels.
+         * @param height Height of the image in number of pixels.
+         * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+         *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+         */
+        public Builder(@NonNull FileDescriptor fd,
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @InputMode int inputMode) {
+            this(null, fd, width, height, inputMode);
+        }
+
+        private Builder(String path, FileDescriptor fd,
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @InputMode int inputMode) {
+            mPath = path;
+            mFd = fd;
+            mWidth = width;
+            mHeight = height;
+            mInputMode = inputMode;
+        }
+
+        /**
+         * Set the image rotation in degrees.
+         *
+         * @param rotation Rotation angle in degrees (clockwise) of the image, must be 0, 90,
+         *                 180 or 270. Default is 0.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setRotation(@IntRange(from = 0) int rotation) {
+            if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
+                throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
+            }
+            mRotation = rotation;
+            return this;
+        }
+
+        /**
+         * Set whether to enable grid option.
+         *
+         * @param gridEnabled Whether to enable grid option. If enabled, the tile size will be
+         *                    automatically chosen. Default is to enable.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setGridEnabled(boolean gridEnabled) {
+            mGridEnabled = gridEnabled;
+            return this;
+        }
+
+        /**
+         * Set the quality for encoding images.
+         *
+         * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best
+         *                quality supported by this implementation. Default is 100.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setQuality(@IntRange(from = 0, to = 100) int quality) {
+            if (quality < 0 || quality > 100) {
+                throw new IllegalArgumentException("Invalid quality: " + quality);
+            }
+            mQuality = quality;
+            return this;
+        }
+
+        /**
+         * Set the maximum number of images to write.
+         *
+         * @param maxImages Max number of images to write. Frames exceeding this number will not be
+         *                  written to file. The writing can be stopped earlier before this number
+         *                  of images are written by {@link #stop(long)}, except for the input mode
+         *                  of {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be
+         *                  specified (via {@link #setInputEndOfStreamTimestamp(long)} and reached.
+         *                  Default is 1.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setMaxImages(@IntRange(from = 1) int maxImages) {
+            if (maxImages <= 0) {
+                throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
+            }
+            mMaxImages = maxImages;
+            return this;
+        }
+
+        /**
+         * Set the primary image index.
+         *
+         * @param primaryIndex Index of the image that should be marked as primary, must be within
+         *                     range [0, maxImages - 1] inclusive. Default is 0.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setPrimaryIndex(@IntRange(from = 0) int primaryIndex) {
+            mPrimaryIndex = primaryIndex;
+            return this;
+        }
+
+        /**
+         * Provide a handler for the AvifWriter to use.
+         *
+         * @param handler If not null, client will receive all callbacks on the handler's looper.
+         *                Otherwise, client will receive callbacks on a looper created by the
+         *                writer. Default is null.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setHandler(@Nullable Handler handler) {
+            mHandler = handler;
+            return this;
+        }
+
+        /**
+         * Provide a setting for the AvifWriter to use high bit-depth or not.
+         *
+         * @param highBitDepthEnabled Whether to enable high bit-depth mode. Default is false, if
+         *                            true, AvifWriter will encode with high bit-depth.
+         * @return this Builder object.
+         */
+        public @NonNull Builder setHighBitDepthEnabled(boolean highBitDepthEnabled) {
+            mHighBitDepthEnabled = highBitDepthEnabled;
+            return this;
+        }
+
+        /**
+         * Build a AvifWriter object.
+         *
+         * @return a AvifWriter object built according to the specifications.
+         * @throws IOException if failed to create the writer, possibly due to failure to create
+         *                     {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
+         */
+        public @NonNull AvifWriter build() throws IOException {
+            return new AvifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
+                mMaxImages, mPrimaryIndex, mInputMode, mHandler, mHighBitDepthEnabled);
+        }
+    }
+
+    @SuppressLint("WrongConstant")
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    AvifWriter(@NonNull String path,
+        @NonNull FileDescriptor fd,
+        int width,
+        int height,
+        int rotation,
+        boolean gridEnabled,
+        int quality,
+        int maxImages,
+        int primaryIndex,
+        @InputMode int inputMode,
+        @Nullable Handler handler,
+        boolean highBitDepthEnabled) throws IOException {
+        super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
+            handler, highBitDepthEnabled);
+
+        if (DEBUG) {
+            Log.d(TAG, "width: " + width
+                + ", height: " + height
+                + ", rotation: " + rotation
+                + ", gridEnabled: " + gridEnabled
+                + ", quality: " + quality
+                + ", maxImages: " + maxImages
+                + ", primaryIndex: " + primaryIndex
+                + ", inputMode: " + inputMode);
+        }
+
+        // set to 1 initially, and wait for output format to know for sure
+        mNumTiles = 1;
+
+        mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
+            : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
+
+        mEncoder = new AvifEncoder(width, height, gridEnabled, quality,
+            mInputMode, mHandler, new WriterCallback(), highBitDepthEnabled);
+    }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
index 35d34d4..c69e002 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
@@ -25,6 +25,8 @@
 import android.util.Log;
 import android.view.Surface;
 
+import androidx.annotation.NonNull;
+
 import java.util.Objects;
 
 /**
@@ -52,18 +54,22 @@
      * Creates an EglWindowSurface from a Surface.
      */
     public EglWindowSurface(Surface surface) {
+        this(surface, false);
+    }
+
+    public EglWindowSurface(Surface surface, boolean useHighBitDepth) {
         if (surface == null) {
             throw new NullPointerException();
         }
         mSurface = surface;
 
-        eglSetup();
+        eglSetup(useHighBitDepth);
     }
 
     /**
      * Prepares EGL. We want a GLES 2.0 context and a surface that supports recording.
      */
-    private void eglSetup() {
+    private void eglSetup(boolean useHighBitDepth) {
         mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
         if (Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
             throw new RuntimeException("unable to get EGL14 display");
@@ -76,27 +82,31 @@
 
         // Configure EGL for recordable and OpenGL ES 2.0.  We want enough RGB bits
         // to minimize artifacts from possible YUV conversion.
-        int[] attribList = {
-                EGL14.EGL_RED_SIZE, 8,
-                EGL14.EGL_GREEN_SIZE, 8,
-                EGL14.EGL_BLUE_SIZE, 8,
-                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
-                EGLExt.EGL_RECORDABLE_ANDROID, 1,
-                EGL14.EGL_NONE
+        int eglColorSize = useHighBitDepth ? 10: 8;
+        int eglAlphaSize = useHighBitDepth ? 2: 0;
+        int recordable = useHighBitDepth ? 0: 1;
+        int[] configAttribList = {
+            EGL14.EGL_RED_SIZE, eglColorSize,
+            EGL14.EGL_GREEN_SIZE, eglColorSize,
+            EGL14.EGL_BLUE_SIZE, eglColorSize,
+            EGL14.EGL_ALPHA_SIZE, eglAlphaSize,
+            EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
+            EGLExt.EGL_RECORDABLE_ANDROID, recordable,
+            EGL14.EGL_NONE
         };
         int[] numConfigs = new int[1];
-        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, mConfigs, 0, mConfigs.length,
-                numConfigs, 0)) {
+        if (!EGL14.eglChooseConfig(mEGLDisplay, configAttribList, 0, mConfigs, 0, mConfigs.length,
+            numConfigs, 0)) {
             throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
         }
 
         // Configure context for OpenGL ES 2.0.
-        int[] attrib_list = {
-                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
-                EGL14.EGL_NONE
+        int[] contextAttribList = {
+            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
+            EGL14.EGL_NONE
         };
         mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mConfigs[0], EGL14.EGL_NO_CONTEXT,
-                attrib_list, 0);
+            contextAttribList, 0);
         checkEglError("eglCreateContext");
         if (mEGLContext == null) {
             throw new RuntimeException("null context");
@@ -188,7 +198,7 @@
     /**
      * Returns the Surface that the MediaCodec receives buffers from.
      */
-    public Surface getSurface() {
+    public @NonNull Surface getSurface() {
         return mSurface;
     }
 
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
new file mode 100644
index 0000000..5a30454
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/EncoderBase.java
@@ -0,0 +1,1058 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.media.Image;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CodecException;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.opengl.GLES20;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+import android.util.Range;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class holds common utilities for {@link HeifEncoder} and {@link AvifEncoder}, and
+ * calls media framework and encodes images into HEIF- or AVIF- compatible samples using
+ * HEVC or AV1 encoder.
+ *
+ * It currently supports three input modes: {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ *
+ * Callback#onOutputFormatChanged(MediaCodec, MediaFormat)} and {@link
+ * Callback#onDrainOutputBuffer(MediaCodec, ByteBuffer)}. If the client
+ * requests to use grid, each tile will be sent back individually.
+ *
+ *
+ *  * HeifEncoder is made a separate class from {@link HeifWriter}, as some more
+ *  * advanced use cases might want to build solutions on top of the HeifEncoder directly.
+ *  * (eg. mux still images and video tracks into a single container).
+ *
+ *
+ * @hide
+ */
+public class EncoderBase implements AutoCloseable,
+    SurfaceTexture.OnFrameAvailableListener {
+    private static final String TAG = "EncoderBase";
+    private static final boolean DEBUG = false;
+
+    private String MIME;
+    private int GRID_WIDTH;
+    private int GRID_HEIGHT;
+    private double MAX_COMPRESS_RATIO;
+    private int INPUT_BUFFER_POOL_SIZE = 2;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+        MediaCodec mEncoder;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final MediaFormat mCodecFormat;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final Callback mCallback;
+    private final HandlerThread mHandlerThread;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final Handler mHandler;
+    private final @InputMode int mInputMode;
+    private final boolean mUseBitDepth10;
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final int mWidth;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mHeight;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mGridWidth;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mGridHeight;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mGridRows;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mGridCols;
+    private final int mNumTiles;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final boolean mUseGrid;
+
+    private int mInputIndex;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+        boolean mInputEOS;
+    private final Rect mSrcRect;
+    private final Rect mDstRect;
+    private ByteBuffer mCurrentBuffer;
+    private final ArrayList<ByteBuffer> mEmptyBuffers = new ArrayList<>();
+    private final ArrayList<ByteBuffer> mFilledBuffers = new ArrayList<>();
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final ArrayList<Integer> mCodecInputBuffers = new ArrayList<>();
+    private final boolean mCopyTiles;
+
+    // Helper for tracking EOS when surface is used
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+        SurfaceEOSTracker mEOSTracker;
+
+    // Below variables are to handle GL copy from client's surface
+    // to encoder surface when tiles are used.
+    private SurfaceTexture mInputTexture;
+    private Surface mInputSurface;
+    private Surface mEncoderSurface;
+    private EglWindowSurface mEncoderEglSurface;
+    private EglRectBlt mRectBlt;
+    private int mTextureId;
+    private final float[] mTmpMatrix = new float[16];
+    private final AtomicBoolean mStopping = new AtomicBoolean(false);
+
+    public static final int INPUT_MODE_BUFFER = HeifWriter.INPUT_MODE_BUFFER;
+    public static final int INPUT_MODE_SURFACE = HeifWriter.INPUT_MODE_SURFACE;
+    public static final int INPUT_MODE_BITMAP = HeifWriter.INPUT_MODE_BITMAP;
+    @IntDef({
+        INPUT_MODE_BUFFER,
+        INPUT_MODE_SURFACE,
+        INPUT_MODE_BITMAP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InputMode {}
+
+    public static abstract class Callback {
+        /**
+         * Called when the output format has changed.
+         *
+         * @param encoder The EncoderBase object.
+         * @param format The new output format.
+         */
+        public abstract void onOutputFormatChanged(
+            @NonNull EncoderBase encoder, @NonNull MediaFormat format);
+
+        /**
+         * Called when an output buffer becomes available.
+         *
+         * @param encoder The EncoderBase object.
+         * @param byteBuffer the available output buffer.
+         */
+        public abstract void onDrainOutputBuffer(
+            @NonNull EncoderBase encoder, @NonNull ByteBuffer byteBuffer);
+
+        /**
+         * Called when encoding reached the end of stream without error.
+         *
+         * @param encoder The EncoderBase object.
+         */
+        public abstract void onComplete(@NonNull EncoderBase encoder);
+
+        /**
+         * Called when encoding hits an error.
+         *
+         * @param encoder The EncoderBase object.
+         * @param e The exception that the codec reported.
+         */
+        public abstract void onError(@NonNull EncoderBase encoder, @NonNull CodecException e);
+    }
+
+    /**
+     * Configure the encoder. Should only be called once.
+     *
+     * @param mimeType mime type. Currently it supports "HEIC" and "AVIF".
+     * @param width Width of the image.
+     * @param height Height of the image.
+     * @param useGrid Whether to encode image into tiles. If enabled, tile size will be
+     *                automatically chosen.
+     * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
+     *                supported by this implementation (which often results in larger file size).
+     * @param inputMode The input type of this encoding session.
+     * @param handler If not null, client will receive all callbacks on the handler's looper.
+     *                Otherwise, client will receive callbacks on a looper created by us.
+     * @param cb The callback to receive various messages from the heif encoder.
+     */
+    protected EncoderBase(@NonNull String mimeType, int width, int height, boolean useGrid,
+        int quality, @InputMode int inputMode,
+        @Nullable Handler handler, @NonNull Callback cb,
+        boolean useBitDepth10) throws IOException {
+        if (DEBUG)
+            Log.d(TAG, "width: " + width + ", height: " + height +
+                ", useGrid: " + useGrid + ", quality: " + quality +
+                ", inputMode: " + inputMode +
+                ", useBitDepth10: " + String.valueOf(useBitDepth10));
+
+        if (width < 0 || height < 0 || quality < 0 || quality > 100) {
+            throw new IllegalArgumentException("invalid encoder inputs");
+        }
+
+        switch (mimeType) {
+            case "HEIC":
+                MIME = mimeType;
+                GRID_WIDTH = HeifEncoder.GRID_WIDTH;
+                GRID_HEIGHT = HeifEncoder.GRID_HEIGHT;
+                MAX_COMPRESS_RATIO = HeifEncoder.MAX_COMPRESS_RATIO;
+                break;
+            case "AVIF":
+                MIME = mimeType;
+                GRID_WIDTH = AvifEncoder.GRID_WIDTH;
+                GRID_HEIGHT = AvifEncoder.GRID_HEIGHT;
+                MAX_COMPRESS_RATIO = AvifEncoder.MAX_COMPRESS_RATIO;
+                break;
+            default:
+                Log.e(TAG, "Not supported mime type: " + mimeType);
+        }
+
+        boolean useHeicEncoder = false;
+        MediaCodecInfo.CodecCapabilities caps = null;
+        switch (MIME) {
+            case "HEIC":
+                try {
+                    mEncoder = MediaCodec.createEncoderByType(
+                        MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+                    caps = mEncoder.getCodecInfo().getCapabilitiesForType(
+                        MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+                    // If the HEIC encoder can't support the size, fall back to HEVC encoder.
+                    if (!caps.getVideoCapabilities().isSizeSupported(width, height)) {
+                        mEncoder.release();
+                        mEncoder = null;
+                        throw new Exception();
+                    }
+                    useHeicEncoder = true;
+                } catch (Exception e) {
+                    mEncoder = MediaCodec.createByCodecName(HeifEncoder.findHevcFallback());
+                    caps = mEncoder.getCodecInfo()
+                        .getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+                    // Disable grid if the image is too small
+                    useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
+                    // Always enable grid if the size is too large for the HEVC encoder
+                    useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
+                }
+                break;
+            case "AVIF":
+                mEncoder = MediaCodec.createByCodecName(AvifEncoder.findAv1Fallback());
+                caps = mEncoder.getCodecInfo()
+                    .getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AV1);
+                // Disable grid if the image is too small
+                useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
+                // Always enable grid if the size is too large for the AV1 encoder
+                useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
+                break;
+            default:
+                Log.e(TAG, "Not supported mime type: " + MIME);
+        }
+
+        mInputMode = inputMode;
+        mUseBitDepth10 = useBitDepth10;
+        mCallback = cb;
+
+        Looper looper = (handler != null) ? handler.getLooper() : null;
+        if (looper == null) {
+            mHandlerThread = new HandlerThread("HeifEncoderThread",
+                Process.THREAD_PRIORITY_FOREGROUND);
+            mHandlerThread.start();
+            looper = mHandlerThread.getLooper();
+        } else {
+            mHandlerThread = null;
+        }
+        mHandler = new Handler(looper);
+        boolean useSurfaceInternally =
+            (inputMode == INPUT_MODE_SURFACE) || (inputMode == INPUT_MODE_BITMAP);
+        int colorFormat = useSurfaceInternally ? CodecCapabilities.COLOR_FormatSurface :
+                (useBitDepth10 ? CodecCapabilities.COLOR_FormatYUVP010 :
+                CodecCapabilities.COLOR_FormatYUV420Flexible);
+        mCopyTiles = (useGrid && !useHeicEncoder) || (inputMode == INPUT_MODE_BITMAP);
+
+        mWidth = width;
+        mHeight = height;
+        mUseGrid = useGrid;
+
+        int gridWidth, gridHeight, gridRows, gridCols;
+
+        if (useGrid) {
+            gridWidth = GRID_WIDTH;
+            gridHeight = GRID_HEIGHT;
+            gridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
+            gridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
+        } else {
+            gridWidth = mWidth;
+            gridHeight = mHeight;
+            gridRows = 1;
+            gridCols = 1;
+        }
+
+        MediaFormat codecFormat;
+        if (useHeicEncoder) {
+            codecFormat = MediaFormat.createVideoFormat(
+                MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, mWidth, mHeight);
+        } else {
+            codecFormat = MediaFormat.createVideoFormat(
+                MediaFormat.MIMETYPE_VIDEO_HEVC, gridWidth, gridHeight);
+        }
+
+        if (useGrid) {
+            codecFormat.setInteger(MediaFormat.KEY_TILE_WIDTH, gridWidth);
+            codecFormat.setInteger(MediaFormat.KEY_TILE_HEIGHT, gridHeight);
+            codecFormat.setInteger(MediaFormat.KEY_GRID_COLUMNS, gridCols);
+            codecFormat.setInteger(MediaFormat.KEY_GRID_ROWS, gridRows);
+        }
+
+        if (useHeicEncoder) {
+            mGridWidth = width;
+            mGridHeight = height;
+            mGridRows = 1;
+            mGridCols = 1;
+        } else {
+            mGridWidth = gridWidth;
+            mGridHeight = gridHeight;
+            mGridRows = gridRows;
+            mGridCols = gridCols;
+        }
+        mNumTiles = mGridRows * mGridCols;
+
+        codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
+        codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
+        codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
+
+        // When we're doing tiles, set the operating rate higher as the size
+        // is small, otherwise set to the normal 30fps.
+        if (mNumTiles > 1) {
+            codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 120);
+        } else {
+            codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 30);
+        }
+
+        if (useSurfaceInternally && !mCopyTiles) {
+            // Use fixed PTS gap and disable backward frame drop
+            Log.d(TAG, "Setting fixed pts gap");
+            codecFormat.setLong(MediaFormat.KEY_MAX_PTS_GAP_TO_ENCODER, -1000000);
+        }
+
+        MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
+
+        if (encoderCaps.isBitrateModeSupported(
+            MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+            Log.d(TAG, "Setting bitrate mode to constant quality");
+            Range<Integer> qualityRange = encoderCaps.getQualityRange();
+            Log.d(TAG, "Quality range: " + qualityRange);
+            codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);
+            codecFormat.setInteger(MediaFormat.KEY_QUALITY, (int) (qualityRange.getLower() +
+                (qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
+        } else {
+            if (encoderCaps.isBitrateModeSupported(
+                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
+                Log.d(TAG, "Setting bitrate mode to constant bitrate");
+                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+            } else { // assume VBR
+                Log.d(TAG, "Setting bitrate mode to variable bitrate");
+                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
+            }
+            // Calculate the bitrate based on image dimension, max compression ratio and quality.
+            // Note that we set the frame rate to the number of tiles, so the bitrate would be the
+            // intended bits for one image.
+            int bitrate = caps.getVideoCapabilities().getBitrateRange().clamp(
+                (int) (width * height * 1.5 * 8 * MAX_COMPRESS_RATIO * quality / 100.0f));
+            codecFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
+        }
+
+        mCodecFormat = codecFormat;
+
+        mDstRect = new Rect(0, 0, mGridWidth, mGridHeight);
+        mSrcRect = new Rect();
+    }
+
+    /**
+     * Finish setting up the encoder.
+     * Call MediaCodec.configure() method so that mEncoder enters configured stage, then add input
+     * surface or add input buffers if needed.
+     *
+     * Note: this method must be called after the constructor.
+     */
+    protected void finishSettingUpEncoder(boolean useBitDepth10) {
+        boolean useSurfaceInternally =
+            (mInputMode == INPUT_MODE_SURFACE) || (mInputMode == INPUT_MODE_BITMAP);
+
+        mEncoder.configure(mCodecFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+        if (useSurfaceInternally) {
+            mEncoderSurface = mEncoder.createInputSurface();
+
+            mEOSTracker = new SurfaceEOSTracker(mCopyTiles);
+
+            if (mCopyTiles) {
+                mEncoderEglSurface = new EglWindowSurface(mEncoderSurface, useBitDepth10);
+                mEncoderEglSurface.makeCurrent();
+
+                mRectBlt = new EglRectBlt(
+                    new Texture2dProgram((mInputMode == INPUT_MODE_BITMAP)
+                        ? Texture2dProgram.TEXTURE_2D
+                        : Texture2dProgram.TEXTURE_EXT),
+                    mWidth, mHeight);
+
+                mTextureId = mRectBlt.createTextureObject();
+
+                if (mInputMode == INPUT_MODE_SURFACE) {
+                    // use single buffer mode to block on input
+                    mInputTexture = new SurfaceTexture(mTextureId, true);
+                    mInputTexture.setOnFrameAvailableListener(this);
+                    mInputTexture.setDefaultBufferSize(mWidth, mHeight);
+                    mInputSurface = new Surface(mInputTexture);
+                }
+
+                // make uncurrent since onFrameAvailable could be called on arbituray thread.
+                // making the context current on a different thread will cause error.
+                mEncoderEglSurface.makeUnCurrent();
+            } else {
+                mInputSurface = mEncoderSurface;
+            }
+        } else {
+            for (int i = 0; i < INPUT_BUFFER_POOL_SIZE; i++) {
+                int bufferSize = mUseBitDepth10 ? mWidth * mHeight * 3 : mWidth * mHeight * 3 / 2;
+                mEmptyBuffers.add(ByteBuffer.allocateDirect(bufferSize));
+            }
+        }
+    }
+
+    /**
+     * Copies from source frame to encoder inputs using GL. The source could be either
+     * client's input surface, or the input bitmap loaded to texture.
+     */
+    private void copyTilesGL() {
+        GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
+
+        for (int row = 0; row < mGridRows; row++) {
+            for (int col = 0; col < mGridCols; col++) {
+                int left = col * mGridWidth;
+                int top = row * mGridHeight;
+                mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+                try {
+                    mRectBlt.copyRect(mTextureId, Texture2dProgram.V_FLIP_MATRIX, mSrcRect);
+                } catch (RuntimeException e) {
+                    // EGL copy could throw if the encoder input surface is no longer valid
+                    // after encoder is released. This is not an error because we're already
+                    // stopping (either after EOS is received or requested by client).
+                    if (mStopping.get()) {
+                        return;
+                    }
+                    throw e;
+                }
+                mEncoderEglSurface.setPresentationTime(
+                    1000 * computePresentationTime(mInputIndex++));
+                mEncoderEglSurface.swapBuffers();
+            }
+        }
+    }
+
+    @Override
+    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+        synchronized (this) {
+            if (mEncoderEglSurface == null) {
+                return;
+            }
+
+            mEncoderEglSurface.makeCurrent();
+
+            surfaceTexture.updateTexImage();
+            surfaceTexture.getTransformMatrix(mTmpMatrix);
+
+            long timestampNs = surfaceTexture.getTimestamp();
+
+            if (DEBUG) Log.d(TAG, "onFrameAvailable: timestampUs " + (timestampNs / 1000));
+
+            boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(timestampNs,
+                computePresentationTime(mInputIndex + mNumTiles - 1));
+
+            if (takeFrame) {
+                copyTilesGL();
+            }
+
+            surfaceTexture.releaseTexImage();
+
+            // make uncurrent since the onFrameAvailable could be called on arbituray thread.
+            // making the context current on a different thread will cause error.
+            mEncoderEglSurface.makeUnCurrent();
+        }
+    }
+
+    /**
+     * Start the encoding process.
+     */
+    public void start() {
+        mEncoder.start();
+    }
+
+    /**
+     * Add one YUV buffer to be encoded. This might block if the encoder can't process the input
+     * buffers fast enough.
+     *
+     * After the call returns, the client can reuse the data array.
+     *
+     * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
+     *               only support YUV_420_888.
+     *
+     * @param data byte array containing the YUV data. If the format has more than one planes,
+     *             they must be concatenated.
+     */
+    public void addYuvBuffer(int format, @NonNull byte[] data) {
+        if (mInputMode != INPUT_MODE_BUFFER) {
+            throw new IllegalStateException(
+                "addYuvBuffer is only allowed in buffer input mode");
+        }
+        if ((mUseBitDepth10 && format != ImageFormat.YCBCR_P010)
+                || (!mUseBitDepth10 && format != ImageFormat.YUV_420_888)) {
+            throw new IllegalStateException("Wrong color format.");
+        }
+        if (data == null
+                || (mUseBitDepth10 && data.length != mWidth * mHeight * 3)
+                || (!mUseBitDepth10 && data.length != mWidth * mHeight * 3 / 2)) {
+            throw new IllegalArgumentException("invalid data");
+        }
+        addYuvBufferInternal(data);
+    }
+
+    /**
+     * Retrieves the input surface for encoding.
+     *
+     * Will only return valid value if configured to use surface input.
+     */
+    public @NonNull Surface getInputSurface() {
+        if (mInputMode != INPUT_MODE_SURFACE) {
+            throw new IllegalStateException(
+                "getInputSurface is only allowed in surface input mode");
+        }
+        return mInputSurface;
+    }
+
+    /**
+     * Sets the timestamp (in nano seconds) of the last input frame to encode. Frames with
+     * timestamps larger than the specified value will not be encoded. However, if a frame
+     * already started encoding when this is set, all tiles within that frame will be encoded.
+     *
+     * This method only applies when surface is used.
+     */
+    public void setEndOfInputStreamTimestamp(long timestampNs) {
+        if (mInputMode != INPUT_MODE_SURFACE) {
+            throw new IllegalStateException(
+                "setEndOfInputStreamTimestamp is only allowed in surface input mode");
+        }
+        if (mEOSTracker != null) {
+            mEOSTracker.updateInputEOSTime(timestampNs);
+        }
+    }
+
+    /**
+     * Adds one bitmap to be encoded.
+     */
+    public void addBitmap(@NonNull Bitmap bitmap) {
+        if (mInputMode != INPUT_MODE_BITMAP) {
+            throw new IllegalStateException("addBitmap is only allowed in bitmap input mode");
+        }
+
+        boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(
+            computePresentationTime(mInputIndex) * 1000,
+            computePresentationTime(mInputIndex + mNumTiles - 1));
+
+        if (!takeFrame) return;
+
+        synchronized (this) {
+            if (mEncoderEglSurface == null) {
+                return;
+            }
+
+            mEncoderEglSurface.makeCurrent();
+
+            mRectBlt.loadTexture(mTextureId, bitmap);
+
+            copyTilesGL();
+
+            // make uncurrent since the onFrameAvailable could be called on arbituray thread.
+            // making the context current on a different thread will cause error.
+            mEncoderEglSurface.makeUnCurrent();
+        }
+    }
+
+    /**
+     * Sends input EOS to the encoder. Result will be notified asynchronously via
+     * {@link Callback#onComplete(EncoderBase)} if encoder reaches EOS without error, or
+     * {@link Callback#onError(EncoderBase, CodecException)} otherwise.
+     */
+    public void stopAsync() {
+        if (mInputMode == INPUT_MODE_BITMAP) {
+            // here we simply set the EOS timestamp to 0, so that the cut off will be the last
+            // bitmap ever added.
+            mEOSTracker.updateInputEOSTime(0);
+        } else if (mInputMode == INPUT_MODE_BUFFER) {
+            addYuvBufferInternal(null);
+        }
+    }
+
+    /**
+     * Generates the presentation time for input frame N, in microseconds.
+     * The timestamp advances 1 sec for every whole frame.
+     */
+    private long computePresentationTime(int frameIndex) {
+        return 132 + (long)frameIndex * 1000000 / mNumTiles;
+    }
+
+    /**
+     * Obtains one empty input buffer and copies the data into it. Before input
+     * EOS is sent, this would block until the data is copied. After input EOS
+     * is sent, this would return immediately.
+     */
+    private void addYuvBufferInternal(@Nullable byte[] data) {
+        ByteBuffer buffer = acquireEmptyBuffer();
+        if (buffer == null) {
+            return;
+        }
+        buffer.clear();
+        if (data != null) {
+            buffer.put(data);
+        }
+        buffer.flip();
+        synchronized (mFilledBuffers) {
+            mFilledBuffers.add(buffer);
+        }
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                maybeCopyOneTileYUV();
+            }
+        });
+    }
+
+    /**
+     * Routine to copy one tile if we have both input and codec buffer available.
+     *
+     * Must be called on the handler looper that also handles the MediaCodec callback.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void maybeCopyOneTileYUV() {
+        ByteBuffer currentBuffer;
+        while ((currentBuffer = getCurrentBuffer()) != null && !mCodecInputBuffers.isEmpty()) {
+            int index = mCodecInputBuffers.remove(0);
+
+            // 0-length input means EOS.
+            boolean inputEOS = (mInputIndex % mNumTiles == 0) && (currentBuffer.remaining() == 0);
+
+            if (!inputEOS) {
+                Image image = mEncoder.getInputImage(index);
+                int left = mGridWidth * (mInputIndex % mGridCols);
+                int top = mGridHeight * (mInputIndex / mGridCols % mGridRows);
+                mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+                copyOneTileYUV(currentBuffer, image, mWidth, mHeight, mSrcRect, mDstRect,
+                        mUseBitDepth10);
+            }
+
+            mEncoder.queueInputBuffer(index, 0,
+                inputEOS ? 0 : mEncoder.getInputBuffer(index).capacity(),
+                computePresentationTime(mInputIndex++),
+                inputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+
+            if (inputEOS || mInputIndex % mNumTiles == 0) {
+                returnEmptyBufferAndNotify(inputEOS);
+            }
+        }
+    }
+
+    /**
+     * Copies from a rect from src buffer to dst image.
+     * TOOD: This will be replaced by JNI.
+     */
+    private static void copyOneTileYUV(ByteBuffer srcBuffer, Image dstImage,
+            int srcWidth, int srcHeight, Rect srcRect, Rect dstRect, boolean useBitDepth10) {
+        if (srcRect.width() != dstRect.width() || srcRect.height() != dstRect.height()) {
+            throw new IllegalArgumentException("src and dst rect size are different!");
+        }
+        if (srcWidth % 2 != 0      || srcHeight % 2 != 0      ||
+            srcRect.left % 2 != 0  || srcRect.top % 2 != 0    ||
+            srcRect.right % 2 != 0 || srcRect.bottom % 2 != 0 ||
+            dstRect.left % 2 != 0  || dstRect.top % 2 != 0    ||
+            dstRect.right % 2 != 0 || dstRect.bottom % 2 != 0) {
+            throw new IllegalArgumentException("src or dst are not aligned!");
+        }
+
+        Image.Plane[] planes = dstImage.getPlanes();
+        if (useBitDepth10) {
+            for (int n = 0; n < planes.length; n++) {
+                ByteBuffer dstBuffer = planes[n].getBuffer();
+                int colStride = planes[n].getPixelStride();
+                int copyWidth = Math.min(srcRect.width(), srcWidth - srcRect.left);
+                int copyHeight = Math.min(srcRect.height(), srcHeight - srcRect.top);
+                int srcPlanePos = 0, div = 1;
+                if (n > 0) {
+                    div = 2;
+                    srcPlanePos = srcWidth * srcHeight;
+                }
+                for (int i = 0; i < copyHeight / div; i++) {
+                    srcBuffer.position(srcPlanePos +
+                        (i + srcRect.top / div) * srcWidth + srcRect.left / div);
+                    dstBuffer.position((i + dstRect.top / div) * planes[n].getRowStride()
+                        + dstRect.left * colStride / div);
+
+                    for (int j = 0; j < copyWidth; j++) {
+                        dstBuffer.put(srcBuffer.get());
+                        if (colStride > 1 && j != copyWidth - 1) {
+                            dstBuffer.position(dstBuffer.position() + colStride - 1);
+                        }
+                    }
+                }
+            }
+        } else {
+            for (int n = 0; n < planes.length; n++) {
+                ByteBuffer dstBuffer = planes[n].getBuffer();
+                int colStride = planes[n].getPixelStride();
+                int copyWidth = Math.min(srcRect.width(), srcWidth - srcRect.left);
+                int copyHeight = Math.min(srcRect.height(), srcHeight - srcRect.top);
+                int srcPlanePos = 0, div = 1;
+                if (n > 0) {
+                    div = 2;
+                    srcPlanePos = srcWidth * srcHeight * (n + 3) / 4;
+                }
+                for (int i = 0; i < copyHeight / div; i++) {
+                    srcBuffer.position(srcPlanePos +
+                        (i + srcRect.top / div) * srcWidth / div + srcRect.left / div);
+                    dstBuffer.position((i + dstRect.top / div) * planes[n].getRowStride()
+                        + dstRect.left * colStride / div);
+
+                    for (int j = 0; j < copyWidth / div; j++) {
+                        dstBuffer.put(srcBuffer.get());
+                        if (colStride > 1 && j != copyWidth / div - 1) {
+                            dstBuffer.position(dstBuffer.position() + colStride - 1);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private ByteBuffer acquireEmptyBuffer() {
+        synchronized (mEmptyBuffers) {
+            // wait for an empty input buffer first
+            while (!mInputEOS && mEmptyBuffers.isEmpty()) {
+                try {
+                    mEmptyBuffers.wait();
+                } catch (InterruptedException e) {}
+            }
+
+            // if already EOS, return null to stop further encoding.
+            return mInputEOS ? null : mEmptyBuffers.remove(0);
+        }
+    }
+
+    /**
+     * Routine to get the current input buffer to copy from.
+     * Only called on callback handler thread.
+     */
+    private ByteBuffer getCurrentBuffer() {
+        if (!mInputEOS && mCurrentBuffer == null) {
+            synchronized (mFilledBuffers) {
+                mCurrentBuffer = mFilledBuffers.isEmpty() ?
+                    null : mFilledBuffers.remove(0);
+            }
+        }
+        return mInputEOS ? null : mCurrentBuffer;
+    }
+
+    /**
+     * Routine to put the consumed input buffer back into the empty buffer pool.
+     * Only called on callback handler thread.
+     */
+    private void returnEmptyBufferAndNotify(boolean inputEOS) {
+        synchronized (mEmptyBuffers) {
+            mInputEOS |= inputEOS;
+            mEmptyBuffers.add(mCurrentBuffer);
+            mEmptyBuffers.notifyAll();
+        }
+        mCurrentBuffer = null;
+    }
+
+    /**
+     * Routine to release all resources. Must be run on the same looper that
+     * handles the MediaCodec callbacks.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void stopInternal() {
+        if (DEBUG) Log.d(TAG, "stopInternal");
+
+        // set stopping, so that the tile copy would bail out
+        // if it hits failure after this point.
+        mStopping.set(true);
+
+        // after start, mEncoder is only accessed on handler, so no need to sync.
+        try {
+            if (mEncoder != null) {
+                mEncoder.stop();
+                mEncoder.release();
+            }
+        } catch (Exception e) {
+        } finally {
+            mEncoder = null;
+        }
+
+        // unblock the addBuffer() if we're tearing down before EOS is sent.
+        synchronized (mEmptyBuffers) {
+            mInputEOS = true;
+            mEmptyBuffers.notifyAll();
+        }
+
+        // Clean up surface and Egl related refs. This lock must come after encoder
+        // release. When we're closing, we insert stopInternal() at the front of queue
+        // so that the shutdown can be processed promptly, this means there might be
+        // some output available requests queued after this. As the tile copies trying
+        // to finish the current frame, there is a chance is might get stuck because
+        // those outputs were not returned. Shutting down the encoder will make break
+        // the tile copier out of that.
+        synchronized(this) {
+            try {
+                if (mRectBlt != null) {
+                    mRectBlt.release(false);
+                }
+            } catch (Exception e) {
+            } finally {
+                mRectBlt = null;
+            }
+
+            try {
+                if (mEncoderEglSurface != null) {
+                    // Note that this frees mEncoderSurface too. If mEncoderEglSurface is not
+                    // there, client is responsible to release the input surface it got from us,
+                    // we don't release mEncoderSurface here.
+                    mEncoderEglSurface.release();
+                }
+            } catch (Exception e) {
+            } finally {
+                mEncoderEglSurface = null;
+            }
+
+            try {
+                if (mInputTexture != null) {
+                    mInputTexture.release();
+                }
+            } catch (Exception e) {
+            } finally {
+                mInputTexture = null;
+            }
+        }
+    }
+
+    /**
+     * This class handles EOS for surface or bitmap inputs.
+     *
+     * When encoding from surface or bitmap, we can't call
+     * {@link MediaCodec#signalEndOfInputStream()} immediately after input is drawn, since this
+     * could drop all pending frames in the buffer queue. When there are tiles, this could leave
+     * us a partially encoded image.
+     *
+     * So here we track the EOS status by timestamps, and only signal EOS to the encoder
+     * when we collected all images we need.
+     *
+     * Since this is updated from multiple threads ({@link #setEndOfInputStreamTimestamp(long)},
+     * {@link EncoderCallback#onOutputBufferAvailable(MediaCodec, int, BufferInfo)},
+     * {@link #addBitmap(Bitmap)} and {@link #onFrameAvailable(SurfaceTexture)}), it must be fully
+     * synchronized.
+     *
+     * Note that when buffer input is used, the EOS flag is set in
+     * {@link EncoderCallback#onInputBufferAvailable(MediaCodec, int)} and this class is not used.
+     */
+    private class SurfaceEOSTracker {
+        private static final boolean DEBUG_EOS = false;
+
+        final boolean mCopyTiles;
+        long mInputEOSTimeNs = -1;
+        long mLastInputTimeNs = -1;
+        long mEncoderEOSTimeUs = -1;
+        long mLastEncoderTimeUs = -1;
+        long mLastOutputTimeUs = -1;
+        boolean mSignaled;
+
+        SurfaceEOSTracker(boolean copyTiles) {
+            mCopyTiles = copyTiles;
+        }
+
+        synchronized void updateInputEOSTime(long timestampNs) {
+            if (DEBUG_EOS) Log.d(TAG, "updateInputEOSTime: " + timestampNs);
+
+            if (mCopyTiles) {
+                if (mInputEOSTimeNs < 0) {
+                    mInputEOSTimeNs = timestampNs;
+                }
+            } else {
+                if (mEncoderEOSTimeUs < 0) {
+                    mEncoderEOSTimeUs = timestampNs / 1000;
+                }
+            }
+            updateEOSLocked();
+        }
+
+        synchronized boolean updateLastInputAndEncoderTime(long inputTimeNs, long encoderTimeUs) {
+            if (DEBUG_EOS) Log.d(TAG,
+                "updateLastInputAndEncoderTime: " + inputTimeNs + ", " + encoderTimeUs);
+
+            boolean shouldTakeFrame = mInputEOSTimeNs < 0 || inputTimeNs <= mInputEOSTimeNs;
+            if (shouldTakeFrame) {
+                mLastEncoderTimeUs = encoderTimeUs;
+            }
+            mLastInputTimeNs = inputTimeNs;
+            updateEOSLocked();
+            return shouldTakeFrame;
+        }
+
+        synchronized void updateLastOutputTime(long outputTimeUs) {
+            if (DEBUG_EOS) Log.d(TAG, "updateLastOutputTime: " + outputTimeUs);
+
+            mLastOutputTimeUs = outputTimeUs;
+            updateEOSLocked();
+        }
+
+        private void updateEOSLocked() {
+            if (mSignaled) {
+                return;
+            }
+            if (mEncoderEOSTimeUs < 0) {
+                if (mInputEOSTimeNs >= 0 && mLastInputTimeNs >= mInputEOSTimeNs) {
+                    if (mLastEncoderTimeUs < 0) {
+                        doSignalEOSLocked();
+                        return;
+                    }
+                    // mEncoderEOSTimeUs tracks the timestamp of the last output buffer we
+                    // will wait for. When that buffer arrives, encoder will be signalled EOS.
+                    mEncoderEOSTimeUs = mLastEncoderTimeUs;
+                    if (DEBUG_EOS) Log.d(TAG,
+                        "updateEOSLocked: mEncoderEOSTimeUs " + mEncoderEOSTimeUs);
+                }
+            }
+            if (mEncoderEOSTimeUs >= 0 && mEncoderEOSTimeUs <= mLastOutputTimeUs) {
+                doSignalEOSLocked();
+            }
+        }
+
+        private void doSignalEOSLocked() {
+            if (DEBUG_EOS) Log.d(TAG, "doSignalEOSLocked");
+
+            mHandler.post(new Runnable() {
+                @Override public void run() {
+                    if (mEncoder != null) {
+                        mEncoder.signalEndOfInputStream();
+                    }
+                }
+            });
+
+            mSignaled = true;
+        }
+    }
+
+
+    /**
+     * MediaCodec callback for HEVC/AV1 encoding.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    abstract class EncoderCallback extends MediaCodec.Callback {
+        private boolean mOutputEOS;
+
+        @Override
+        public void onInputBufferAvailable(MediaCodec codec, int index) {
+            if (codec != mEncoder || mInputEOS) return;
+
+            if (DEBUG) Log.d(TAG, "onInputBufferAvailable: " + index);
+            mCodecInputBuffers.add(index);
+            maybeCopyOneTileYUV();
+        }
+
+        @Override
+        public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
+            if (codec != mEncoder || mOutputEOS) return;
+
+            if (DEBUG) {
+                Log.d(TAG, "onOutputBufferAvailable: " + index
+                    + ", time " + info.presentationTimeUs
+                    + ", size " + info.size
+                    + ", flags " + info.flags);
+            }
+
+            if ((info.size > 0) && ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)) {
+                ByteBuffer outputBuffer = codec.getOutputBuffer(index);
+
+                // reset position as addBuffer() modifies it
+                outputBuffer.position(info.offset);
+                outputBuffer.limit(info.offset + info.size);
+
+                if (mEOSTracker != null) {
+                    mEOSTracker.updateLastOutputTime(info.presentationTimeUs);
+                }
+
+                mCallback.onDrainOutputBuffer(EncoderBase.this, outputBuffer);
+            }
+
+            mOutputEOS |= ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
+
+            codec.releaseOutputBuffer(index, false);
+
+            if (mOutputEOS) {
+                stopAndNotify(null);
+            }
+        }
+
+        @Override
+        public void onError(MediaCodec codec, CodecException e) {
+            if (codec != mEncoder) return;
+
+            Log.e(TAG, "onError: " + e);
+            stopAndNotify(e);
+        }
+
+        private void stopAndNotify(@Nullable CodecException e) {
+            stopInternal();
+            if (e == null) {
+                mCallback.onComplete(EncoderBase.this);
+            } else {
+                mCallback.onError(EncoderBase.this, e);
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        // unblock the addBuffer() if we're tearing down before EOS is sent.
+        synchronized (mEmptyBuffers) {
+            mInputEOS = true;
+            mEmptyBuffers.notifyAll();
+        }
+
+        mHandler.postAtFrontOfQueue(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    stopInternal();
+                } catch (Exception e) {
+                    // We don't want to crash when closing.
+                }
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index 5e08a73..6ab3111 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -16,36 +16,20 @@
 
 package androidx.heifwriter;
 
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
-import android.media.Image;
 import android.media.MediaCodec;
-import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.CodecException;
 import android.media.MediaCodecInfo;
 import android.media.MediaCodecInfo.CodecCapabilities;
 import android.media.MediaCodecList;
 import android.media.MediaFormat;
-import android.opengl.GLES20;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Process;
 import android.util.Log;
 import android.util.Range;
-import android.view.Surface;
 
-import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * This class encodes images into HEIF-compatible samples using HEVC encoder.
@@ -64,115 +48,16 @@
  *
  * @hide
  */
-public final class HeifEncoder implements AutoCloseable,
-        SurfaceTexture.OnFrameAvailableListener {
+public final class HeifEncoder extends EncoderBase {
     private static final String TAG = "HeifEncoder";
     private static final boolean DEBUG = false;
 
-    private static final int GRID_WIDTH = 512;
-    private static final int GRID_HEIGHT = 512;
-    private static final double MAX_COMPRESS_RATIO = 0.25f;
-    private static final int INPUT_BUFFER_POOL_SIZE = 2;
+    protected static final int GRID_WIDTH = 512;
+    protected static final int GRID_HEIGHT = 512;
+    protected static final double MAX_COMPRESS_RATIO = 0.25f;
 
     private static final MediaCodecList sMCL =
-            new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    MediaCodec mEncoder;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final Callback mCallback;
-    private final HandlerThread mHandlerThread;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final Handler mHandler;
-    private final @InputMode int mInputMode;
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mWidth;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mHeight;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mGridWidth;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mGridHeight;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mGridRows;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mGridCols;
-    private final int mNumTiles;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final boolean mUseGrid;
-
-    private int mInputIndex;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    boolean mInputEOS;
-    private final Rect mSrcRect;
-    private final Rect mDstRect;
-    private ByteBuffer mCurrentBuffer;
-    private final ArrayList<ByteBuffer> mEmptyBuffers = new ArrayList<>();
-    private final ArrayList<ByteBuffer> mFilledBuffers = new ArrayList<>();
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ArrayList<Integer> mCodecInputBuffers = new ArrayList<>();
-
-    // Helper for tracking EOS when surface is used
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    SurfaceEOSTracker mEOSTracker;
-
-    // Below variables are to handle GL copy from client's surface
-    // to encoder surface when tiles are used.
-    private SurfaceTexture mInputTexture;
-    private Surface mInputSurface;
-    private Surface mEncoderSurface;
-    private EglWindowSurface mEncoderEglSurface;
-    private EglRectBlt mRectBlt;
-    private int mTextureId;
-    private final float[] mTmpMatrix = new float[16];
-    private final AtomicBoolean mStopping = new AtomicBoolean(false);
-
-    public static final int INPUT_MODE_BUFFER = HeifWriter.INPUT_MODE_BUFFER;
-    public static final int INPUT_MODE_SURFACE = HeifWriter.INPUT_MODE_SURFACE;
-    public static final int INPUT_MODE_BITMAP = HeifWriter.INPUT_MODE_BITMAP;
-    @IntDef({
-        INPUT_MODE_BUFFER,
-        INPUT_MODE_SURFACE,
-        INPUT_MODE_BITMAP,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface InputMode {}
-
-    public static abstract class Callback {
-        /**
-         * Called when the output format has changed.
-         *
-         * @param encoder The HeifEncoder object.
-         * @param format The new output format.
-         */
-        public abstract void onOutputFormatChanged(
-                @NonNull HeifEncoder encoder, @NonNull MediaFormat format);
-
-        /**
-         * Called when an output buffer becomes available.
-         *
-         * @param encoder The HeifEncoder object.
-         * @param byteBuffer the available output buffer.
-         */
-        public abstract void onDrainOutputBuffer(
-                @NonNull HeifEncoder encoder, @NonNull ByteBuffer byteBuffer);
-
-        /**
-         * Called when encoding reached the end of stream without error.
-         *
-         * @param encoder The HeifEncoder object.
-         */
-        public abstract void onComplete(@NonNull HeifEncoder encoder);
-
-        /**
-         * Called when encoding hits an error.
-         *
-         * @param encoder The HeifEncoder object.
-         * @param e The exception that the codec reported.
-         */
-        public abstract void onError(@NonNull HeifEncoder encoder, @NonNull CodecException e);
-    }
+        new MediaCodecList(MediaCodecList.REGULAR_CODECS);
 
     /**
      * Configure the heif encoding session. Should only be called once.
@@ -189,198 +74,15 @@
      * @param cb The callback to receive various messages from the heif encoder.
      */
     public HeifEncoder(int width, int height, boolean useGrid,
-                       int quality, @InputMode int inputMode,
-                       @Nullable Handler handler, @NonNull Callback cb) throws IOException {
-        if (DEBUG) Log.d(TAG, "width: " + width + ", height: " + height +
-                ", useGrid: " + useGrid + ", quality: " + quality + ", inputMode: " + inputMode);
-
-        if (width < 0 || height < 0 || quality < 0 || quality > 100) {
-            throw new IllegalArgumentException("invalid encoder inputs");
-        }
-
-        // Disable grid if the image is too small
-        useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
-
-        boolean useHeicEncoder = false;
-        MediaCodecInfo.CodecCapabilities caps = null;
-        try {
-            mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
-            caps = mEncoder.getCodecInfo().getCapabilitiesForType(
-                    MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
-            // If the HEIC encoder can't support the size, fall back to HEVC encoder.
-            if (!caps.getVideoCapabilities().isSizeSupported(width, height)) {
-                mEncoder.release();
-                mEncoder = null;
-                throw new Exception();
-            }
-            useHeicEncoder = true;
-        } catch (Exception e) {
-            mEncoder = MediaCodec.createByCodecName(findHevcFallback());
-            caps = mEncoder.getCodecInfo().getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
-            // Always enable grid if the size is too large for the HEVC encoder
-            useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
-        }
-
-        mInputMode = inputMode;
-
-        mCallback = cb;
-
-        Looper looper = (handler != null) ? handler.getLooper() : null;
-        if (looper == null) {
-            mHandlerThread = new HandlerThread("HeifEncoderThread",
-                    Process.THREAD_PRIORITY_FOREGROUND);
-            mHandlerThread.start();
-            looper = mHandlerThread.getLooper();
-        } else {
-            mHandlerThread = null;
-        }
-        mHandler = new Handler(looper);
-        boolean useSurfaceInternally =
-                (inputMode == INPUT_MODE_SURFACE) || (inputMode == INPUT_MODE_BITMAP);
-        int colorFormat = useSurfaceInternally ? CodecCapabilities.COLOR_FormatSurface :
-                CodecCapabilities.COLOR_FormatYUV420Flexible;
-        boolean copyTiles = (useGrid && !useHeicEncoder) || (inputMode == INPUT_MODE_BITMAP);
-
-        mWidth = width;
-        mHeight = height;
-        mUseGrid = useGrid;
-
-        int gridWidth, gridHeight, gridRows, gridCols;
-
-        if (useGrid) {
-            gridWidth = GRID_WIDTH;
-            gridHeight = GRID_HEIGHT;
-            gridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
-            gridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
-        } else {
-            gridWidth = mWidth;
-            gridHeight = mHeight;
-            gridRows = 1;
-            gridCols = 1;
-        }
-
-        MediaFormat codecFormat;
-        if (useHeicEncoder) {
-            codecFormat = MediaFormat.createVideoFormat(
-                    MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, mWidth, mHeight);
-        } else {
-            codecFormat = MediaFormat.createVideoFormat(
-                    MediaFormat.MIMETYPE_VIDEO_HEVC, gridWidth, gridHeight);
-        }
-
-        if (useGrid) {
-            codecFormat.setInteger(MediaFormat.KEY_TILE_WIDTH, gridWidth);
-            codecFormat.setInteger(MediaFormat.KEY_TILE_HEIGHT, gridHeight);
-            codecFormat.setInteger(MediaFormat.KEY_GRID_COLUMNS, gridCols);
-            codecFormat.setInteger(MediaFormat.KEY_GRID_ROWS, gridRows);
-        }
-
-        if (useHeicEncoder) {
-            mGridWidth = width;
-            mGridHeight = height;
-            mGridRows = 1;
-            mGridCols = 1;
-        } else {
-            mGridWidth = gridWidth;
-            mGridHeight = gridHeight;
-            mGridRows = gridRows;
-            mGridCols = gridCols;
-        }
-        mNumTiles = mGridRows * mGridCols;
-
-        codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
-        codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
-        codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
-
-        // When we're doing tiles, set the operating rate higher as the size
-        // is small, otherwise set to the normal 30fps.
-        if (mNumTiles > 1) {
-            codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 120);
-        } else {
-            codecFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, 30);
-        }
-
-        if (useSurfaceInternally && !copyTiles) {
-            // Use fixed PTS gap and disable backward frame drop
-            Log.d(TAG, "Setting fixed pts gap");
-            codecFormat.setLong(MediaFormat.KEY_MAX_PTS_GAP_TO_ENCODER, -1000000);
-        }
-
-        MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
-
-        if (encoderCaps.isBitrateModeSupported(
-                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
-            Log.d(TAG, "Setting bitrate mode to constant quality");
-            Range<Integer> qualityRange = encoderCaps.getQualityRange();
-            Log.d(TAG, "Quality range: " + qualityRange);
-            codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
-                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);
-            codecFormat.setInteger(MediaFormat.KEY_QUALITY, (int) (qualityRange.getLower() +
-                            (qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
-        } else {
-            if (encoderCaps.isBitrateModeSupported(
-                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
-                Log.d(TAG, "Setting bitrate mode to constant bitrate");
-                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
-                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
-            } else { // assume VBR
-                Log.d(TAG, "Setting bitrate mode to variable bitrate");
-                codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
-                        MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
-            }
-            // Calculate the bitrate based on image dimension, max compression ratio and quality.
-            // Note that we set the frame rate to the number of tiles, so the bitrate would be the
-            // intended bits for one image.
-            int bitrate = caps.getVideoCapabilities().getBitrateRange().clamp(
-                    (int) (width * height * 1.5 * 8 * MAX_COMPRESS_RATIO * quality / 100.0f));
-            codecFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
-        }
-
-        mEncoder.setCallback(new EncoderCallback(), mHandler);
-        mEncoder.configure(codecFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
-
-        if (useSurfaceInternally) {
-            mEncoderSurface = mEncoder.createInputSurface();
-
-            mEOSTracker = new SurfaceEOSTracker(copyTiles);
-
-            if (copyTiles) {
-                mEncoderEglSurface = new EglWindowSurface(mEncoderSurface);
-                mEncoderEglSurface.makeCurrent();
-
-                mRectBlt = new EglRectBlt(
-                        new Texture2dProgram((inputMode == INPUT_MODE_BITMAP)
-                                ? Texture2dProgram.TEXTURE_2D
-                                : Texture2dProgram.TEXTURE_EXT),
-                        mWidth, mHeight);
-
-                mTextureId = mRectBlt.createTextureObject();
-
-                if (inputMode == INPUT_MODE_SURFACE) {
-                    // use single buffer mode to block on input
-                    mInputTexture = new SurfaceTexture(mTextureId, true);
-                    mInputTexture.setOnFrameAvailableListener(this);
-                    mInputTexture.setDefaultBufferSize(mWidth, mHeight);
-                    mInputSurface = new Surface(mInputTexture);
-                }
-
-                // make uncurrent since onFrameAvailable could be called on arbituray thread.
-                // making the context current on a different thread will cause error.
-                mEncoderEglSurface.makeUnCurrent();
-            } else {
-                mInputSurface = mEncoderSurface;
-            }
-        } else {
-            for (int i = 0; i < INPUT_BUFFER_POOL_SIZE; i++) {
-                mEmptyBuffers.add(ByteBuffer.allocateDirect(mWidth * mHeight * 3 / 2));
-            }
-        }
-
-        mDstRect = new Rect(0, 0, mGridWidth, mGridHeight);
-        mSrcRect = new Rect();
+            int quality, @InputMode int inputMode,
+            @Nullable Handler handler, @NonNull Callback cb) throws IOException {
+        super("HEIC", width, height, useGrid, quality, inputMode, handler, cb,
+            /* useBitDepth10 */ false);
+        mEncoder.setCallback(new HevcEncoderCallback(), mHandler);
+        finishSettingUpEncoder(/* useBitDepth10 */ false);
     }
 
-    private String findHevcFallback() {
+    protected static String findHevcFallback() {
         String hevc = null; // first HEVC encoder
         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
             if (!info.isEncoder()) {
@@ -396,7 +98,7 @@
                 continue;
             }
             if (caps.getEncoderCapabilities().isBitrateModeSupported(
-                    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
+                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
                 // Encoder that supports CQ mode is preferred over others,
                 // return the first encoder that supports CQ mode.
                 // (No need to check if it's hw based, it's already listed in
@@ -410,508 +112,12 @@
         // If no encoders support CQ, return the first HEVC encoder.
         return hevc;
     }
-    /**
-     * Copies from source frame to encoder inputs using GL. The source could be either
-     * client's input surface, or the input bitmap loaded to texture.
-     */
-    private void copyTilesGL() {
-        GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
-
-        for (int row = 0; row < mGridRows; row++) {
-            for (int col = 0; col < mGridCols; col++) {
-                int left = col * mGridWidth;
-                int top = row * mGridHeight;
-                mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
-                try {
-                    mRectBlt.copyRect(mTextureId, Texture2dProgram.V_FLIP_MATRIX, mSrcRect);
-                } catch (RuntimeException e) {
-                    // EGL copy could throw if the encoder input surface is no longer valid
-                    // after encoder is released. This is not an error because we're already
-                    // stopping (either after EOS is received or requested by client).
-                    if (mStopping.get()) {
-                        return;
-                    }
-                    throw e;
-                }
-                mEncoderEglSurface.setPresentationTime(
-                        1000 * computePresentationTime(mInputIndex++));
-                mEncoderEglSurface.swapBuffers();
-            }
-        }
-    }
-
-    @Override
-    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
-        synchronized (this) {
-            if (mEncoderEglSurface == null) {
-                return;
-            }
-
-            mEncoderEglSurface.makeCurrent();
-
-            surfaceTexture.updateTexImage();
-            surfaceTexture.getTransformMatrix(mTmpMatrix);
-
-            long timestampNs = surfaceTexture.getTimestamp();
-
-            if (DEBUG) Log.d(TAG, "onFrameAvailable: timestampUs " + (timestampNs / 1000));
-
-            boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(timestampNs,
-                    computePresentationTime(mInputIndex + mNumTiles - 1));
-
-            if (takeFrame) {
-                copyTilesGL();
-            }
-
-            surfaceTexture.releaseTexImage();
-
-            // make uncurrent since the onFrameAvailable could be called on arbituray thread.
-            // making the context current on a different thread will cause error.
-            mEncoderEglSurface.makeUnCurrent();
-        }
-    }
-
-    /**
-     * Start the encoding process.
-     */
-    public void start() {
-        mEncoder.start();
-    }
-
-    /**
-     * Add one YUV buffer to be encoded. This might block if the encoder can't process the input
-     * buffers fast enough.
-     *
-     * After the call returns, the client can reuse the data array.
-     *
-     * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
-     *               only support YUV_420_888.
-     *
-     * @param data byte array containing the YUV data. If the format has more than one planes,
-     *             they must be concatenated.
-     */
-    public void addYuvBuffer(int format, @NonNull byte[] data) {
-        if (mInputMode != INPUT_MODE_BUFFER) {
-            throw new IllegalStateException(
-                    "addYuvBuffer is only allowed in buffer input mode");
-        }
-        if (data == null || data.length != mWidth * mHeight * 3 / 2) {
-            throw new IllegalArgumentException("invalid data");
-        }
-        addYuvBufferInternal(data);
-    }
-
-    /**
-     * Retrieves the input surface for encoding.
-     *
-     * Will only return valid value if configured to use surface input.
-     */
-    public @NonNull Surface getInputSurface() {
-        if (mInputMode != INPUT_MODE_SURFACE) {
-            throw new IllegalStateException(
-                    "getInputSurface is only allowed in surface input mode");
-        }
-        return mInputSurface;
-    }
-
-    /**
-     * Sets the timestamp (in nano seconds) of the last input frame to encode. Frames with
-     * timestamps larger than the specified value will not be encoded. However, if a frame
-     * already started encoding when this is set, all tiles within that frame will be encoded.
-     *
-     * This method only applies when surface is used.
-     */
-    public void setEndOfInputStreamTimestamp(long timestampNs) {
-        if (mInputMode != INPUT_MODE_SURFACE) {
-            throw new IllegalStateException(
-                    "setEndOfInputStreamTimestamp is only allowed in surface input mode");
-        }
-        if (mEOSTracker != null) {
-            mEOSTracker.updateInputEOSTime(timestampNs);
-        }
-    }
-
-    /**
-     * Adds one bitmap to be encoded.
-     */
-    public void addBitmap(@NonNull Bitmap bitmap) {
-        if (mInputMode != INPUT_MODE_BITMAP) {
-            throw new IllegalStateException("addBitmap is only allowed in bitmap input mode");
-        }
-
-        boolean takeFrame = mEOSTracker.updateLastInputAndEncoderTime(
-                computePresentationTime(mInputIndex) * 1000,
-                computePresentationTime(mInputIndex + mNumTiles - 1));
-
-        if (!takeFrame) return;
-
-        synchronized (this) {
-            if (mEncoderEglSurface == null) {
-                return;
-            }
-
-            mEncoderEglSurface.makeCurrent();
-
-            mRectBlt.loadTexture(mTextureId, bitmap);
-
-            copyTilesGL();
-
-            // make uncurrent since the onFrameAvailable could be called on arbituray thread.
-            // making the context current on a different thread will cause error.
-            mEncoderEglSurface.makeUnCurrent();
-        }
-    }
-
-    /**
-     * Sends input EOS to the encoder. Result will be notified asynchronously via
-     * {@link Callback#onComplete(HeifEncoder)} if encoder reaches EOS without error, or
-     * {@link Callback#onError(HeifEncoder, CodecException)} otherwise.
-     */
-    public void stopAsync() {
-        if (mInputMode == INPUT_MODE_BITMAP) {
-            // here we simply set the EOS timestamp to 0, so that the cut off will be the last
-            // bitmap ever added.
-            mEOSTracker.updateInputEOSTime(0);
-        } else if (mInputMode == INPUT_MODE_BUFFER) {
-            addYuvBufferInternal(null);
-        }
-    }
-
-    /**
-     * Generates the presentation time for input frame N, in microseconds.
-     * The timestamp advances 1 sec for every whole frame.
-     */
-    private long computePresentationTime(int frameIndex) {
-        return 132 + (long)frameIndex * 1000000 / mNumTiles;
-    }
-
-    /**
-     * Obtains one empty input buffer and copies the data into it. Before input
-     * EOS is sent, this would block until the data is copied. After input EOS
-     * is sent, this would return immediately.
-     */
-    private void addYuvBufferInternal(@Nullable byte[] data) {
-        ByteBuffer buffer = acquireEmptyBuffer();
-        if (buffer == null) {
-            return;
-        }
-        buffer.clear();
-        if (data != null) {
-            buffer.put(data);
-        }
-        buffer.flip();
-        synchronized (mFilledBuffers) {
-            mFilledBuffers.add(buffer);
-        }
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                maybeCopyOneTileYUV();
-            }
-        });
-    }
-
-    /**
-     * Routine to copy one tile if we have both input and codec buffer available.
-     *
-     * Must be called on the handler looper that also handles the MediaCodec callback.
-     */
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void maybeCopyOneTileYUV() {
-        ByteBuffer currentBuffer;
-        while ((currentBuffer = getCurrentBuffer()) != null && !mCodecInputBuffers.isEmpty()) {
-            int index = mCodecInputBuffers.remove(0);
-
-            // 0-length input means EOS.
-            boolean inputEOS = (mInputIndex % mNumTiles == 0) && (currentBuffer.remaining() == 0);
-
-            if (!inputEOS) {
-                Image image = mEncoder.getInputImage(index);
-                int left = mGridWidth * (mInputIndex % mGridCols);
-                int top = mGridHeight * (mInputIndex / mGridCols % mGridRows);
-                mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
-                copyOneTileYUV(currentBuffer, image, mWidth, mHeight, mSrcRect, mDstRect);
-            }
-
-            mEncoder.queueInputBuffer(index, 0,
-                    inputEOS ? 0 : mEncoder.getInputBuffer(index).capacity(),
-                    computePresentationTime(mInputIndex++),
-                    inputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
-
-            if (inputEOS || mInputIndex % mNumTiles == 0) {
-                returnEmptyBufferAndNotify(inputEOS);
-            }
-        }
-    }
-
-    /**
-     * Copies from a rect from src buffer to dst image.
-     * TOOD: This will be replaced by JNI.
-     */
-    private static void copyOneTileYUV(
-            ByteBuffer srcBuffer, Image dstImage,
-            int srcWidth, int srcHeight,
-            Rect srcRect, Rect dstRect) {
-        if (srcRect.width() != dstRect.width() || srcRect.height() != dstRect.height()) {
-            throw new IllegalArgumentException("src and dst rect size are different!");
-        }
-        if (srcWidth % 2 != 0      || srcHeight % 2 != 0      ||
-                srcRect.left % 2 != 0  || srcRect.top % 2 != 0    ||
-                srcRect.right % 2 != 0 || srcRect.bottom % 2 != 0 ||
-                dstRect.left % 2 != 0  || dstRect.top % 2 != 0    ||
-                dstRect.right % 2 != 0 || dstRect.bottom % 2 != 0) {
-            throw new IllegalArgumentException("src or dst are not aligned!");
-        }
-
-        Image.Plane[] planes = dstImage.getPlanes();
-        for (int n = 0; n < planes.length; n++) {
-            ByteBuffer dstBuffer = planes[n].getBuffer();
-            int colStride = planes[n].getPixelStride();
-            int copyWidth = Math.min(srcRect.width(), srcWidth - srcRect.left);
-            int copyHeight = Math.min(srcRect.height(), srcHeight - srcRect.top);
-            int srcPlanePos = 0, div = 1;
-            if (n > 0) {
-                div = 2;
-                srcPlanePos = srcWidth * srcHeight * (n + 3) / 4;
-            }
-            for (int i = 0; i < copyHeight / div; i++) {
-                srcBuffer.position(srcPlanePos +
-                        (i + srcRect.top / div) * srcWidth / div + srcRect.left / div);
-                dstBuffer.position((i + dstRect.top / div) * planes[n].getRowStride()
-                        + dstRect.left * colStride / div);
-
-                for (int j = 0; j < copyWidth / div; j++) {
-                    dstBuffer.put(srcBuffer.get());
-                    if (colStride > 1 && j != copyWidth / div - 1) {
-                        dstBuffer.position(dstBuffer.position() + colStride - 1);
-                    }
-                }
-            }
-        }
-    }
-
-    private ByteBuffer acquireEmptyBuffer() {
-        synchronized (mEmptyBuffers) {
-            // wait for an empty input buffer first
-            while (!mInputEOS && mEmptyBuffers.isEmpty()) {
-                try {
-                    mEmptyBuffers.wait();
-                } catch (InterruptedException e) {}
-            }
-
-            // if already EOS, return null to stop further encoding.
-            return mInputEOS ? null : mEmptyBuffers.remove(0);
-        }
-    }
-
-    /**
-     * Routine to get the current input buffer to copy from.
-     * Only called on callback handler thread.
-     */
-    private ByteBuffer getCurrentBuffer() {
-        if (!mInputEOS && mCurrentBuffer == null) {
-            synchronized (mFilledBuffers) {
-                mCurrentBuffer = mFilledBuffers.isEmpty() ?
-                        null : mFilledBuffers.remove(0);
-            }
-        }
-        return mInputEOS ? null : mCurrentBuffer;
-    }
-
-    /**
-     * Routine to put the consumed input buffer back into the empty buffer pool.
-     * Only called on callback handler thread.
-     */
-    private void returnEmptyBufferAndNotify(boolean inputEOS) {
-        synchronized (mEmptyBuffers) {
-            mInputEOS |= inputEOS;
-            mEmptyBuffers.add(mCurrentBuffer);
-            mEmptyBuffers.notifyAll();
-        }
-        mCurrentBuffer = null;
-    }
-
-    /**
-     * Routine to release all resources. Must be run on the same looper that
-     * handles the MediaCodec callbacks.
-     */
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void stopInternal() {
-        if (DEBUG) Log.d(TAG, "stopInternal");
-
-        // set stopping, so that the tile copy would bail out
-        // if it hits failure after this point.
-        mStopping.set(true);
-
-        // after start, mEncoder is only accessed on handler, so no need to sync.
-        try {
-            if (mEncoder != null) {
-                mEncoder.stop();
-                mEncoder.release();
-            }
-        } catch (Exception e) {
-        } finally {
-            mEncoder = null;
-        }
-
-        // unblock the addBuffer() if we're tearing down before EOS is sent.
-        synchronized (mEmptyBuffers) {
-            mInputEOS = true;
-            mEmptyBuffers.notifyAll();
-        }
-
-        // Clean up surface and Egl related refs. This lock must come after encoder
-        // release. When we're closing, we insert stopInternal() at the front of queue
-        // so that the shutdown can be processed promptly, this means there might be
-        // some output available requests queued after this. As the tile copies trying
-        // to finish the current frame, there is a chance is might get stuck because
-        // those outputs were not returned. Shutting down the encoder will make break
-        // the tile copier out of that.
-        synchronized(this) {
-            try {
-                if (mRectBlt != null) {
-                    mRectBlt.release(false);
-                }
-            } catch (Exception e) {
-            } finally {
-                mRectBlt = null;
-            }
-
-            try {
-                if (mEncoderEglSurface != null) {
-                    // Note that this frees mEncoderSurface too. If mEncoderEglSurface is not
-                    // there, client is responsible to release the input surface it got from us,
-                    // we don't release mEncoderSurface here.
-                    mEncoderEglSurface.release();
-                }
-            } catch (Exception e) {
-            } finally {
-                mEncoderEglSurface = null;
-            }
-
-            try {
-                if (mInputTexture != null) {
-                    mInputTexture.release();
-                }
-            } catch (Exception e) {
-            } finally {
-                mInputTexture = null;
-            }
-        }
-    }
-
-    /**
-     * This class handles EOS for surface or bitmap inputs.
-     *
-     * When encoding from surface or bitmap, we can't call {@link MediaCodec#signalEndOfInputStream()}
-     * immediately after input is drawn, since this could drop all pending frames in the
-     * buffer queue. When there are tiles, this could leave us a partially encoded image.
-     *
-     * So here we track the EOS status by timestamps, and only signal EOS to the encoder
-     * when we collected all images we need.
-     *
-     * Since this is updated from multiple threads ({@link #setEndOfInputStreamTimestamp(long)},
-     * {@link EncoderCallback#onOutputBufferAvailable(MediaCodec, int, BufferInfo)},
-     * {@link #addBitmap(Bitmap)} and {@link #onFrameAvailable(SurfaceTexture)}), it must be fully
-     * synchronized.
-     *
-     * Note that when buffer input is used, the EOS flag is set in
-     * {@link EncoderCallback#onInputBufferAvailable(MediaCodec, int)} and this class is not used.
-     */
-    private class SurfaceEOSTracker {
-        private static final boolean DEBUG_EOS = false;
-
-        final boolean mCopyTiles;
-        long mInputEOSTimeNs = -1;
-        long mLastInputTimeNs = -1;
-        long mEncoderEOSTimeUs = -1;
-        long mLastEncoderTimeUs = -1;
-        long mLastOutputTimeUs = -1;
-        boolean mSignaled;
-
-        SurfaceEOSTracker(boolean copyTiles) {
-            mCopyTiles = copyTiles;
-        }
-
-        synchronized void updateInputEOSTime(long timestampNs) {
-            if (DEBUG_EOS) Log.d(TAG, "updateInputEOSTime: " + timestampNs);
-
-            if (mCopyTiles) {
-                if (mInputEOSTimeNs < 0) {
-                    mInputEOSTimeNs = timestampNs;
-                }
-            } else {
-                if (mEncoderEOSTimeUs < 0) {
-                    mEncoderEOSTimeUs = timestampNs / 1000;
-                }
-            }
-            updateEOSLocked();
-        }
-
-        synchronized boolean updateLastInputAndEncoderTime(long inputTimeNs, long encoderTimeUs) {
-            if (DEBUG_EOS) Log.d(TAG,
-                    "updateLastInputAndEncoderTime: " + inputTimeNs + ", " + encoderTimeUs);
-
-            boolean shouldTakeFrame = mInputEOSTimeNs < 0 || inputTimeNs <= mInputEOSTimeNs;
-            if (shouldTakeFrame) {
-                mLastEncoderTimeUs = encoderTimeUs;
-            }
-            mLastInputTimeNs = inputTimeNs;
-            updateEOSLocked();
-            return shouldTakeFrame;
-        }
-
-        synchronized void updateLastOutputTime(long outputTimeUs) {
-            if (DEBUG_EOS) Log.d(TAG, "updateLastOutputTime: " + outputTimeUs);
-
-            mLastOutputTimeUs = outputTimeUs;
-            updateEOSLocked();
-        }
-
-        private void updateEOSLocked() {
-            if (mSignaled) {
-                return;
-            }
-            if (mEncoderEOSTimeUs < 0) {
-                if (mInputEOSTimeNs >= 0 && mLastInputTimeNs >= mInputEOSTimeNs) {
-                    if (mLastEncoderTimeUs < 0) {
-                        doSignalEOSLocked();
-                        return;
-                    }
-                    // mEncoderEOSTimeUs tracks the timestamp of the last output buffer we
-                    // will wait for. When that buffer arrives, encoder will be signalled EOS.
-                    mEncoderEOSTimeUs = mLastEncoderTimeUs;
-                    if (DEBUG_EOS) Log.d(TAG,
-                            "updateEOSLocked: mEncoderEOSTimeUs " + mEncoderEOSTimeUs);
-                }
-            }
-            if (mEncoderEOSTimeUs >= 0 && mEncoderEOSTimeUs <= mLastOutputTimeUs) {
-                doSignalEOSLocked();
-            }
-        }
-
-        private void doSignalEOSLocked() {
-            if (DEBUG_EOS) Log.d(TAG, "doSignalEOSLocked");
-
-            mHandler.post(new Runnable() {
-                @Override public void run() {
-                    if (mEncoder != null) {
-                        mEncoder.signalEndOfInputStream();
-                    }
-                }
-            });
-
-            mSignaled = true;
-        }
-    }
 
     /**
      * MediaCodec callback for HEVC encoding.
      */
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    class EncoderCallback extends MediaCodec.Callback {
-        private boolean mOutputEOS;
-
+    protected class HevcEncoderCallback extends EncoderCallback {
         @Override
         public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
             if (codec != mEncoder) return;
@@ -919,7 +125,7 @@
             if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);
 
             if (!MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC.equals(
-                    format.getString(MediaFormat.KEY_MIME))) {
+                format.getString(MediaFormat.KEY_MIME))) {
                 format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
                 format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
                 format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
@@ -934,85 +140,5 @@
 
             mCallback.onOutputFormatChanged(HeifEncoder.this, format);
         }
-
-        @Override
-        public void onInputBufferAvailable(MediaCodec codec, int index) {
-            if (codec != mEncoder || mInputEOS) return;
-
-            if (DEBUG) Log.d(TAG, "onInputBufferAvailable: " + index);
-            mCodecInputBuffers.add(index);
-            maybeCopyOneTileYUV();
-        }
-
-        @Override
-        public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
-            if (codec != mEncoder || mOutputEOS) return;
-
-            if (DEBUG) {
-                Log.d(TAG, "onOutputBufferAvailable: " + index
-                        + ", time " + info.presentationTimeUs
-                        + ", size " + info.size
-                        + ", flags " + info.flags);
-            }
-
-            if ((info.size > 0) && ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)) {
-                ByteBuffer outputBuffer = codec.getOutputBuffer(index);
-
-                // reset position as addBuffer() modifies it
-                outputBuffer.position(info.offset);
-                outputBuffer.limit(info.offset + info.size);
-
-                if (mEOSTracker != null) {
-                    mEOSTracker.updateLastOutputTime(info.presentationTimeUs);
-                }
-
-                mCallback.onDrainOutputBuffer(HeifEncoder.this, outputBuffer);
-            }
-
-            mOutputEOS |= ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
-
-            codec.releaseOutputBuffer(index, false);
-
-            if (mOutputEOS) {
-                stopAndNotify(null);
-            }
-        }
-
-        @Override
-        public void onError(MediaCodec codec, CodecException e) {
-            if (codec != mEncoder) return;
-
-            Log.e(TAG, "onError: " + e);
-            stopAndNotify(e);
-        }
-
-        private void stopAndNotify(@Nullable CodecException e) {
-            stopInternal();
-            if (e == null) {
-                mCallback.onComplete(HeifEncoder.this);
-            } else {
-                mCallback.onError(HeifEncoder.this, e);
-            }
-        }
     }
-
-    @Override
-    public void close() {
-        // unblock the addBuffer() if we're tearing down before EOS is sent.
-        synchronized (mEmptyBuffers) {
-            mInputEOS = true;
-            mEmptyBuffers.notifyAll();
-        }
-
-        mHandler.postAtFrontOfQueue(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    stopInternal();
-                } catch (Exception e) {
-                    // We don't want to crash when closing.
-                }
-            }
-        });
-    }
-}
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index 978654a..49b5b35 100644
--- a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -32,6 +32,7 @@
 import android.view.Surface;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -77,42 +78,17 @@
  *
  * <p>Please refer to the documentations on individual methods for the exact usage.
  */
-public final class HeifWriter implements AutoCloseable {
+@SuppressWarnings("HiddenSuperclass")
+public final class HeifWriter extends WriterBase {
     private static final String TAG = "HeifWriter";
     private static final boolean DEBUG = false;
-    private static final int MUXER_DATA_FLAG = 16;
-
-    private final @InputMode int mInputMode;
-    private final HandlerThread mHandlerThread;
-    private final Handler mHandler;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    int mNumTiles;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mRotation;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mMaxImages;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final int mPrimaryIndex;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    final ResultWaiter mResultWaiter = new ResultWaiter();
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    MediaMuxer mMuxer;
-    private HeifEncoder mHeifEncoder;
-    final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    int[] mTrackIndexArray;
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    int mOutputIndex;
-    private boolean mStarted;
-
-    private final List<Pair<Integer, ByteBuffer>> mExifList = new ArrayList<>();
 
     /**
      * The input mode where the client adds input buffers with YUV data.
      *
      * @see #addYuvBuffer(int, byte[])
      */
-    public static final int INPUT_MODE_BUFFER = 0;
+    public static final int INPUT_MODE_BUFFER = WriterBase.INPUT_MODE_BUFFER;
 
     /**
      * The input mode where the client renders the images to an input Surface
@@ -125,18 +101,18 @@
      *
      * @see #getInputSurface()
      */
-    public static final int INPUT_MODE_SURFACE = 1;
+    public static final int INPUT_MODE_SURFACE = WriterBase.INPUT_MODE_SURFACE;
 
     /**
      * The input mode where the client adds bitmaps.
      *
      * @see #addBitmap(Bitmap)
      */
-    public static final int INPUT_MODE_BITMAP = 2;
+    public static final int INPUT_MODE_BITMAP = WriterBase.INPUT_MODE_BITMAP;
 
     /** @hide */
     @IntDef({
-            INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
+        INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface InputMode {}
@@ -161,13 +137,15 @@
          * Construct a Builder with output specified by its path.
          *
          * @param path Path of the file to be written.
-         * @param width Width of the image.
-         * @param height Height of the image.
+         * @param width Width of the image in number of pixels.
+         * @param height Height of the image in number of pixels.
          * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
          *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
          */
         public Builder(@NonNull String path,
-                       int width, int height, @InputMode int inputMode) {
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @InputMode int inputMode) {
             this(path, null, width, height, inputMode);
         }
 
@@ -175,21 +153,22 @@
          * Construct a Builder with output specified by its file descriptor.
          *
          * @param fd File descriptor of the file to be written.
-         * @param width Width of the image.
-         * @param height Height of the image.
+         * @param width Width of the image in number of pixels.
+         * @param height Height of the image in number of pixels.
          * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
          *                  {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
          */
         public Builder(@NonNull FileDescriptor fd,
-                       int width, int height, @InputMode int inputMode) {
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @InputMode int inputMode) {
             this(null, fd, width, height, inputMode);
         }
 
         private Builder(String path, FileDescriptor fd,
-                        int width, int height, @InputMode int inputMode) {
-            if (width <= 0 || height <= 0) {
-                throw new IllegalArgumentException("Invalid image size: " + width + "x" + height);
-            }
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @InputMode int inputMode) {
             mPath = path;
             mFd = fd;
             mWidth = width;
@@ -200,11 +179,11 @@
         /**
          * Set the image rotation in degrees.
          *
-         * @param rotation Rotation angle (clockwise) of the image, must be 0, 90, 180 or 270.
-         *                 Default is 0.
+         * @param rotation Rotation angle in degrees (clockwise) of the image, must be 0, 90,
+         *                 180 or 270. Default is 0.
          * @return this Builder object.
          */
-        public Builder setRotation(int rotation) {
+        public Builder setRotation(@IntRange(from = 0)  int rotation) {
             if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
                 throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
             }
@@ -231,7 +210,7 @@
          *                quality supported by this implementation. Default is 100.
          * @return this Builder object.
          */
-        public Builder setQuality(int quality) {
+        public Builder setQuality(@IntRange(from = 0, to = 100) int quality) {
             if (quality < 0 || quality > 100) {
                 throw new IllegalArgumentException("Invalid quality: " + quality);
             }
@@ -250,7 +229,7 @@
          *                  Default is 1.
          * @return this Builder object.
          */
-        public Builder setMaxImages(int maxImages) {
+        public Builder setMaxImages(@IntRange(from = 1) int maxImages) {
             if (maxImages <= 0) {
                 throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
             }
@@ -265,10 +244,7 @@
          *                     range [0, maxImages - 1] inclusive. Default is 0.
          * @return this Builder object.
          */
-        public Builder setPrimaryIndex(int primaryIndex) {
-            if (primaryIndex < 0) {
-                throw new IllegalArgumentException("Invalid primaryIndex: " + primaryIndex);
-            }
+        public Builder setPrimaryIndex(@IntRange(from = 0) int primaryIndex) {
             mPrimaryIndex = primaryIndex;
             return this;
         }
@@ -295,426 +271,44 @@
          */
         public HeifWriter build() throws IOException {
             return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
-                    mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+                mMaxImages, mPrimaryIndex, mInputMode, mHandler);
         }
     }
 
     @SuppressLint("WrongConstant")
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     HeifWriter(@NonNull String path,
-                       @NonNull FileDescriptor fd,
-                       int width,
-                       int height,
-                       int rotation,
-                       boolean gridEnabled,
-                       int quality,
-                       int maxImages,
-                       int primaryIndex,
-                       @InputMode int inputMode,
-                       @Nullable Handler handler) throws IOException {
-        if (primaryIndex >= maxImages) {
-            throw new IllegalArgumentException(
-                    "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
-        }
+        @NonNull FileDescriptor fd,
+        int width,
+        int height,
+        int rotation,
+        boolean gridEnabled,
+        int quality,
+        int maxImages,
+        int primaryIndex,
+        @InputMode int inputMode,
+        @Nullable Handler handler) throws IOException {
+        super(rotation, inputMode, maxImages, primaryIndex, gridEnabled, quality,
+            handler, /* highBitDepthEnabled */ false);
 
         if (DEBUG) {
             Log.d(TAG, "width: " + width
-                    + ", height: " + height
-                    + ", rotation: " + rotation
-                    + ", gridEnabled: " + gridEnabled
-                    + ", quality: " + quality
-                    + ", maxImages: " + maxImages
-                    + ", primaryIndex: " + primaryIndex
-                    + ", inputMode: " + inputMode);
+                + ", height: " + height
+                + ", rotation: " + rotation
+                + ", gridEnabled: " + gridEnabled
+                + ", quality: " + quality
+                + ", maxImages: " + maxImages
+                + ", primaryIndex: " + primaryIndex
+                + ", inputMode: " + inputMode);
         }
 
         // set to 1 initially, and wait for output format to know for sure
         mNumTiles = 1;
 
-        mRotation = rotation;
-        mInputMode = inputMode;
-        mMaxImages = maxImages;
-        mPrimaryIndex = primaryIndex;
-
-        Looper looper = (handler != null) ? handler.getLooper() : null;
-        if (looper == null) {
-            mHandlerThread = new HandlerThread("HeifEncoderThread",
-                    Process.THREAD_PRIORITY_FOREGROUND);
-            mHandlerThread.start();
-            looper = mHandlerThread.getLooper();
-        } else {
-            mHandlerThread = null;
-        }
-        mHandler = new Handler(looper);
-
         mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
-                                : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
+            : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
 
-        mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality,
-                mInputMode, mHandler, new HeifCallback());
+        mEncoder = new HeifEncoder(width, height, gridEnabled, quality,
+            mInputMode, mHandler, new WriterCallback());
     }
-
-    /**
-     * Start the heif writer. Can only be called once.
-     *
-     * @throws IllegalStateException if called more than once.
-     */
-    public void start() {
-        checkStarted(false);
-        mStarted = true;
-        mHeifEncoder.start();
-    }
-
-    /**
-     * Add one YUV buffer to the heif file.
-     *
-     * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
-     *               only support YUV_420_888.
-     *
-     * @param data byte array containing the YUV data. If the format has more than one planes,
-     *             they must be concatenated.
-     *
-     * @throws IllegalStateException if not started or not configured to use buffer input.
-     */
-    public void addYuvBuffer(int format, @NonNull byte[] data) {
-        checkStartedAndMode(INPUT_MODE_BUFFER);
-        synchronized (this) {
-            if (mHeifEncoder != null) {
-                mHeifEncoder.addYuvBuffer(format, data);
-            }
-        }
-    }
-
-    /**
-     * Retrieves the input surface for encoding.
-     *
-     * @return the input surface if configured to use surface input.
-     *
-     * @throws IllegalStateException if called after start or not configured to use surface input.
-     */
-    public @NonNull Surface getInputSurface() {
-        checkStarted(false);
-        checkMode(INPUT_MODE_SURFACE);
-        return mHeifEncoder.getInputSurface();
-    }
-
-    /**
-     * Set the timestamp (in nano seconds) of the last input frame to encode.
-     *
-     * This call is only valid for surface input. Client can use this to stop the heif writer
-     * earlier before the maximum number of images are written. If not called, the writer will
-     * only stop when the maximum number of images are written.
-     *
-     * @param timestampNs timestamp (in nano seconds) of the last frame that will be written to the
-     *                    heif file. Frames with timestamps larger than the specified value will not
-     *                    be written. However, if a frame already started encoding when this is set,
-     *                    all tiles within that frame will be encoded.
-     *
-     * @throws IllegalStateException if not started or not configured to use surface input.
-     */
-    public void setInputEndOfStreamTimestamp(long timestampNs) {
-        checkStartedAndMode(INPUT_MODE_SURFACE);
-        synchronized (this) {
-            if (mHeifEncoder != null) {
-                mHeifEncoder.setEndOfInputStreamTimestamp(timestampNs);
-            }
-        }
-    }
-
-    /**
-     * Add one bitmap to the heif file.
-     *
-     * @param bitmap the bitmap to be added to the file.
-     * @throws IllegalStateException if not started or not configured to use bitmap input.
-     */
-    public void addBitmap(@NonNull Bitmap bitmap) {
-        checkStartedAndMode(INPUT_MODE_BITMAP);
-        synchronized (this) {
-            if (mHeifEncoder != null) {
-                mHeifEncoder.addBitmap(bitmap);
-            }
-        }
-    }
-
-    /**
-     * Add Exif data for the specified image. The data must be a valid Exif data block,
-     * starting with "Exif\0\0" followed by the TIFF header (See JEITA CP-3451C Section 4.5.2.)
-     *
-     * @param imageIndex index of the image, must be a valid index for the max number of image
-     *                   specified by {@link Builder#setMaxImages(int)}.
-     * @param exifData byte buffer containing a Exif data block.
-     * @param offset offset of the Exif data block within exifData.
-     * @param length length of the Exif data block.
-     */
-    public void addExifData(int imageIndex, @NonNull byte[] exifData, int offset, int length) {
-        checkStarted(true);
-
-        ByteBuffer buffer = ByteBuffer.allocateDirect(length);
-        buffer.put(exifData, offset, length);
-        buffer.flip();
-        // Put it in a queue, as we might not be able to process it at this time.
-        synchronized (mExifList) {
-            mExifList.add(new Pair<Integer, ByteBuffer>(imageIndex, buffer));
-        }
-        processExifData();
-    }
-
-    @SuppressLint("WrongConstant")
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void processExifData() {
-        if (!mMuxerStarted.get()) {
-            return;
-        }
-
-        while (true) {
-            Pair<Integer, ByteBuffer> entry;
-            synchronized (mExifList) {
-                if (mExifList.isEmpty()) {
-                    return;
-                }
-                entry = mExifList.remove(0);
-            }
-            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
-            info.set(entry.second.position(), entry.second.remaining(), 0, MUXER_DATA_FLAG);
-            mMuxer.writeSampleData(mTrackIndexArray[entry.first], entry.second, info);
-        }
-    }
-
-    /**
-     * Stop the heif writer synchronously. Throws exception if the writer didn't finish writing
-     * successfully. Upon a success return:
-     *
-     * - For buffer and bitmap inputs, all images sent before stop will be written.
-     *
-     * - For surface input, images with timestamp on or before that specified in
-     *   {@link #setInputEndOfStreamTimestamp(long)} will be written. In case where
-     *   {@link #setInputEndOfStreamTimestamp(long)} was never called, stop will block
-     *   until maximum number of images are received.
-     *
-     * @param timeoutMs Maximum time (in microsec) to wait for the writer to complete, with zero
-     *                  indicating waiting indefinitely.
-     * @see #setInputEndOfStreamTimestamp(long)
-     * @throws Exception if encountered error, in which case the output file may not be valid. In
-     *                   particular, {@link TimeoutException} is thrown when timed out, and {@link
-     *                   MediaCodec.CodecException} is thrown when encountered codec error.
-     */
-    public void stop(long timeoutMs) throws Exception {
-        checkStarted(true);
-        synchronized (this) {
-            if (mHeifEncoder != null) {
-                mHeifEncoder.stopAsync();
-            }
-        }
-        mResultWaiter.waitForResult(timeoutMs);
-        processExifData();
-        closeInternal();
-    }
-
-    private void checkStarted(boolean requiredStarted) {
-        if (mStarted != requiredStarted) {
-            throw new IllegalStateException("Already started");
-        }
-    }
-
-    private void checkMode(@InputMode int requiredMode) {
-        if (mInputMode != requiredMode) {
-            throw new IllegalStateException("Not valid in input mode " + mInputMode);
-        }
-    }
-
-    private void checkStartedAndMode(@InputMode int requiredMode) {
-        checkStarted(true);
-        checkMode(requiredMode);
-    }
-
-    /**
-     * Routine to stop and release writer, must be called on the same looper
-     * that receives heif encoder callbacks.
-     */
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    void closeInternal() {
-        if (DEBUG) Log.d(TAG, "closeInternal");
-        // We don't want to crash when closing, catch all exceptions.
-        try {
-            // Muxer could throw exceptions if stop is called without samples.
-            // Don't crash in that case.
-            if (mMuxer != null) {
-                mMuxer.stop();
-                mMuxer.release();
-            }
-        } catch (Exception e) {
-        } finally {
-            mMuxer = null;
-        }
-        try {
-            if (mHeifEncoder != null) {
-                mHeifEncoder.close();
-            }
-        } catch (Exception e) {
-        } finally {
-            synchronized (this) {
-                mHeifEncoder = null;
-            }
-        }
-    }
-
-    /**
-     * Callback from the heif encoder.
-     */
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    class HeifCallback extends HeifEncoder.Callback {
-        private boolean mEncoderStopped;
-        /**
-         * Upon receiving output format from the encoder, add the requested number of
-         * image tracks to the muxer and start the muxer.
-         */
-        @Override
-        public void onOutputFormatChanged(
-                @NonNull HeifEncoder encoder, @NonNull MediaFormat format) {
-            if (mEncoderStopped) return;
-
-            if (DEBUG) {
-                Log.d(TAG, "onOutputFormatChanged: " + format);
-            }
-            if (mTrackIndexArray != null) {
-                stopAndNotify(new IllegalStateException(
-                        "Output format changed after muxer started"));
-                return;
-            }
-
-            try {
-                int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
-                int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
-                mNumTiles = gridRows * gridCols;
-            } catch (NullPointerException | ClassCastException  e) {
-                mNumTiles = 1;
-            }
-
-            // add mMaxImages image tracks of the same format
-            mTrackIndexArray = new int[mMaxImages];
-
-            // set rotation angle
-            if (mRotation > 0) {
-                Log.d(TAG, "setting rotation: " + mRotation);
-                mMuxer.setOrientationHint(mRotation);
-            }
-            for (int i = 0; i < mTrackIndexArray.length; i++) {
-                // mark primary
-                format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
-                mTrackIndexArray[i] = mMuxer.addTrack(format);
-            }
-            mMuxer.start();
-            mMuxerStarted.set(true);
-            processExifData();
-        }
-
-        /**
-         * Upon receiving an output buffer from the encoder (which is one image when
-         * grid is not used, or one tile if grid is used), add that sample to the muxer.
-         */
-        @Override
-        public void onDrainOutputBuffer(
-                @NonNull HeifEncoder encoder, @NonNull ByteBuffer byteBuffer) {
-            if (mEncoderStopped) return;
-
-            if (DEBUG) {
-                Log.d(TAG, "onDrainOutputBuffer: " + mOutputIndex);
-            }
-            if (mTrackIndexArray == null) {
-                stopAndNotify(new IllegalStateException(
-                        "Output buffer received before format info"));
-                return;
-            }
-
-            if (mOutputIndex < mMaxImages * mNumTiles) {
-                MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
-                info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
-                mMuxer.writeSampleData(
-                        mTrackIndexArray[mOutputIndex / mNumTiles], byteBuffer, info);
-            }
-
-            mOutputIndex++;
-
-            // post EOS if reached max number of images allowed.
-            if (mOutputIndex == mMaxImages * mNumTiles) {
-                stopAndNotify(null);
-            }
-        }
-
-        @Override
-        public void onComplete(@NonNull HeifEncoder encoder) {
-            stopAndNotify(null);
-        }
-
-        @Override
-        public void onError(@NonNull HeifEncoder encoder, @NonNull MediaCodec.CodecException e) {
-            stopAndNotify(e);
-        }
-
-        private void stopAndNotify(@Nullable Exception error) {
-            if (mEncoderStopped) return;
-
-            mEncoderStopped = true;
-            mResultWaiter.signalResult(error);
-        }
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static class ResultWaiter {
-        private boolean mDone;
-        private Exception mException;
-
-        synchronized void waitForResult(long timeoutMs) throws Exception {
-            if (timeoutMs < 0) {
-                throw new IllegalArgumentException("timeoutMs is negative");
-            }
-            if (timeoutMs == 0) {
-                while (!mDone) {
-                    try {
-                        wait();
-                    } catch (InterruptedException ex) {}
-                }
-            } else {
-                final long startTimeMs = System.currentTimeMillis();
-                long remainingWaitTimeMs = timeoutMs;
-                // avoid early termination by "spurious" wakeup.
-                while (!mDone && remainingWaitTimeMs > 0) {
-                    try {
-                        wait(remainingWaitTimeMs);
-                    } catch (InterruptedException ex) {}
-                    remainingWaitTimeMs -= (System.currentTimeMillis() - startTimeMs);
-                }
-            }
-            if (!mDone) {
-                mDone = true;
-                mException = new TimeoutException("timed out waiting for result");
-            }
-            if (mException != null) {
-                throw mException;
-            }
-        }
-
-        synchronized void signalResult(@Nullable Exception e) {
-            if (!mDone) {
-                mDone = true;
-                mException = e;
-                notifyAll();
-            }
-        }
-    }
-
-    @Override
-    public void close() {
-        mHandler.postAtFrontOfQueue(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    closeInternal();
-                } catch (Exception e) {
-                    // If the client called stop() properly, any errors would have been
-                    // reported there. We don't want to crash when closing.
-                }
-            }
-        });
-    }
-}
+}
\ No newline at end of file
diff --git a/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
new file mode 100644
index 0000000..d095a78
--- /dev/null
+++ b/heifwriter/heifwriter/src/main/java/androidx/heifwriter/WriterBase.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2022 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.heifwriter;
+
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class holds common utliities for {@link HeifWriter} and {@link AvifWriter}.
+ *
+ * @hide
+ */
+public class WriterBase implements AutoCloseable {
+    private static final String TAG = "WriterBase";
+    private static final boolean DEBUG = false;
+    private static final int MUXER_DATA_FLAG = 16;
+
+    /**
+     * The input mode where the client adds input buffers with YUV data.
+     *
+     * @see #addYuvBuffer(int, byte[])
+     */
+    protected static final int INPUT_MODE_BUFFER = 0;
+
+    /**
+     * The input mode where the client renders the images to an input Surface
+     * created by the writer.
+     *
+     * The input surface operates in single buffer mode. As a result, for use case
+     * where camera directly outputs to the input surface, this mode will not work
+     * because camera framework requires multiple buffers to operate in a pipeline
+     * fashion.
+     *
+     * @see #getInputSurface()
+     */
+    protected static final int INPUT_MODE_SURFACE = 1;
+
+    /**
+     * The input mode where the client adds bitmaps.
+     *
+     * @see #addBitmap(Bitmap)
+     */
+    protected static final int INPUT_MODE_BITMAP = 2;
+
+    /** @hide */
+    @IntDef({
+        INPUT_MODE_BUFFER, INPUT_MODE_SURFACE, INPUT_MODE_BITMAP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InputMode {}
+
+    protected final @InputMode int mInputMode;
+    protected final boolean mHighBitDepthEnabled;
+    protected final HandlerThread mHandlerThread;
+    protected final Handler mHandler;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected int mNumTiles;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mRotation;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mMaxImages;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected final int mPrimaryIndex;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final ResultWaiter mResultWaiter = new ResultWaiter();
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    @NonNull protected MediaMuxer mMuxer;
+    @NonNull protected EncoderBase mEncoder;
+    final AtomicBoolean mMuxerStarted = new AtomicBoolean(false);
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    int[] mTrackIndexArray;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    int mOutputIndex;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    boolean mGridEnabled;
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    int mQuality;
+    private boolean mStarted;
+
+    private final List<Pair<Integer, ByteBuffer>> mExifList = new ArrayList<>();
+
+    protected WriterBase(int rotation,
+        @InputMode int inputMode,
+        int maxImages,
+        int primaryIndex,
+        boolean gridEnabled,
+        int quality,
+        @Nullable Handler handler,
+        boolean highBitDepthEnabled) throws IOException {
+        if (primaryIndex >= maxImages) {
+            throw new IllegalArgumentException(
+                "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
+        }
+
+        mRotation = rotation;
+        mInputMode = inputMode;
+        mMaxImages = maxImages;
+        mPrimaryIndex = primaryIndex;
+        mGridEnabled = gridEnabled;
+        mQuality = quality;
+        mHighBitDepthEnabled = highBitDepthEnabled;
+
+        Looper looper = (handler != null) ? handler.getLooper() : null;
+        if (looper == null) {
+            mHandlerThread = new HandlerThread("HeifEncoderThread",
+                Process.THREAD_PRIORITY_FOREGROUND);
+            mHandlerThread.start();
+            looper = mHandlerThread.getLooper();
+        } else {
+            mHandlerThread = null;
+        }
+        mHandler = new Handler(looper);
+    }
+
+    /**
+     * Start the heif writer. Can only be called once.
+     *
+     * @throws IllegalStateException if called more than once.
+     */
+    public void start() {
+        checkStarted(false);
+        mStarted = true;
+        mEncoder.start();
+    }
+
+    /**
+     * Add one YUV buffer to the heif file.
+     *
+     * @param format The YUV format as defined in {@link android.graphics.ImageFormat}, currently
+     *               only support YUV_420_888.
+     *
+     * @param data byte array containing the YUV data. If the format has more than one planes,
+     *             they must be concatenated.
+     *
+     * @throws IllegalStateException if not started or not configured to use buffer input.
+     */
+    public void addYuvBuffer(int format, @NonNull byte[] data) {
+        checkStartedAndMode(INPUT_MODE_BUFFER);
+        synchronized (this) {
+            if (mEncoder != null) {
+                mEncoder.addYuvBuffer(format, data);
+            }
+        }
+    }
+
+    /**
+     * Retrieves the input surface for encoding.
+     *
+     * @return the input surface if configured to use surface input.
+     *
+     * @throws IllegalStateException if called after start or not configured to use surface input.
+     */
+    public @NonNull Surface getInputSurface() {
+        checkStarted(false);
+        checkMode(INPUT_MODE_SURFACE);
+        return mEncoder.getInputSurface();
+    }
+
+    /**
+     * Set the timestamp (in nano seconds) of the last input frame to encode.
+     *
+     * This call is only valid for surface input. Client can use this to stop the heif writer
+     * earlier before the maximum number of images are written. If not called, the writer will
+     * only stop when the maximum number of images are written.
+     *
+     * @param timestampNs timestamp (in nano seconds) of the last frame that will be written to the
+     *                    heif file. Frames with timestamps larger than the specified value will not
+     *                    be written. However, if a frame already started encoding when this is set,
+     *                    all tiles within that frame will be encoded.
+     *
+     * @throws IllegalStateException if not started or not configured to use surface input.
+     */
+    public void setInputEndOfStreamTimestamp(@IntRange(from = 0) long timestampNs) {
+        checkStartedAndMode(INPUT_MODE_SURFACE);
+        synchronized (this) {
+            if (mEncoder != null) {
+                mEncoder.setEndOfInputStreamTimestamp(timestampNs);
+            }
+        }
+    }
+
+    /**
+     * Add one bitmap to the heif file.
+     *
+     * @param bitmap the bitmap to be added to the file.
+     * @throws IllegalStateException if not started or not configured to use bitmap input.
+     */
+    public void addBitmap(@NonNull Bitmap bitmap) {
+        checkStartedAndMode(INPUT_MODE_BITMAP);
+        synchronized (this) {
+            if (mEncoder != null) {
+                mEncoder.addBitmap(bitmap);
+            }
+        }
+    }
+
+    /**
+     * Add Exif data for the specified image. The data must be a valid Exif data block,
+     * starting with "Exif\0\0" followed by the TIFF header (See JEITA CP-3451C Section 4.5.2.)
+     *
+     * @param imageIndex index of the image, must be a valid index for the max number of image
+     *                   specified by {@link Builder#setMaxImages(int)}.
+     * @param exifData byte buffer containing a Exif data block.
+     * @param offset offset of the Exif data block within exifData.
+     * @param length length of the Exif data block.
+     */
+    public void addExifData(int imageIndex, @NonNull byte[] exifData, int offset, int length) {
+        checkStarted(true);
+
+        ByteBuffer buffer = ByteBuffer.allocateDirect(length);
+        buffer.put(exifData, offset, length);
+        buffer.flip();
+        // Put it in a queue, as we might not be able to process it at this time.
+        synchronized (mExifList) {
+            mExifList.add(new Pair<Integer, ByteBuffer>(imageIndex, buffer));
+        }
+        processExifData();
+    }
+
+    @SuppressLint("WrongConstant")
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void processExifData() {
+        if (!mMuxerStarted.get()) {
+            return;
+        }
+
+        while (true) {
+            Pair<Integer, ByteBuffer> entry;
+            synchronized (mExifList) {
+                if (mExifList.isEmpty()) {
+                    return;
+                }
+                entry = mExifList.remove(0);
+            }
+            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+            info.set(entry.second.position(), entry.second.remaining(), 0, MUXER_DATA_FLAG);
+            mMuxer.writeSampleData(mTrackIndexArray[entry.first], entry.second, info);
+        }
+    }
+
+    /**
+     * Stop the heif writer synchronously. Throws exception if the writer didn't finish writing
+     * successfully. Upon a success return:
+     *
+     * - For buffer and bitmap inputs, all images sent before stop will be written.
+     *
+     * - For surface input, images with timestamp on or before that specified in
+     *   {@link #setInputEndOfStreamTimestamp(long)} will be written. In case where
+     *   {@link #setInputEndOfStreamTimestamp(long)} was never called, stop will block
+     *   until maximum number of images are received.
+     *
+     * @param timeoutMs Maximum time (in microsec) to wait for the writer to complete, with zero
+     *                  indicating waiting indefinitely.
+     * @see #setInputEndOfStreamTimestamp(long)
+     * @throws Exception if encountered error, in which case the output file may not be valid. In
+     *                   particular, {@link TimeoutException} is thrown when timed out, and {@link
+     *                   MediaCodec.CodecException} is thrown when encountered codec error.
+     */
+    public void stop(@IntRange(from = 0) long timeoutMs) throws Exception {
+        checkStarted(true);
+        synchronized (this) {
+            if (mEncoder != null) {
+                mEncoder.stopAsync();
+            }
+        }
+        mResultWaiter.waitForResult(timeoutMs);
+        processExifData();
+        closeInternal();
+    }
+
+    private void checkStarted(boolean requiredStarted) {
+        if (mStarted != requiredStarted) {
+            throw new IllegalStateException("Already started");
+        }
+    }
+
+    private void checkMode(@InputMode int requiredMode) {
+        if (mInputMode != requiredMode) {
+            throw new IllegalStateException("Not valid in input mode " + mInputMode);
+        }
+    }
+
+    private void checkStartedAndMode(@InputMode int requiredMode) {
+        checkStarted(true);
+        checkMode(requiredMode);
+    }
+
+    /**
+     * Routine to stop and release writer, must be called on the same looper
+     * that receives heif encoder callbacks.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void closeInternal() {
+        if (DEBUG) Log.d(TAG, "closeInternal");
+        // We don't want to crash when closing, catch all exceptions.
+        try {
+            // Muxer could throw exceptions if stop is called without samples.
+            // Don't crash in that case.
+            if (mMuxer != null) {
+                mMuxer.stop();
+                mMuxer.release();
+            }
+        } catch (Exception e) {
+        } finally {
+            mMuxer = null;
+        }
+        try {
+            if (mEncoder != null) {
+                mEncoder.close();
+            }
+        } catch (Exception e) {
+        } finally {
+            synchronized (this) {
+                mEncoder = null;
+            }
+        }
+    }
+
+    /**
+     * Callback from the encoder.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    protected class WriterCallback extends EncoderBase.Callback {
+        private boolean mEncoderStopped;
+        /**
+         * Upon receiving output format from the encoder, add the requested number of
+         * image tracks to the muxer and start the muxer.
+         */
+        @Override
+        public void onOutputFormatChanged(
+            @NonNull EncoderBase encoder, @NonNull MediaFormat format) {
+            if (mEncoderStopped) return;
+
+            if (DEBUG) {
+                Log.d(TAG, "onOutputFormatChanged: " + format);
+            }
+            if (mTrackIndexArray != null) {
+                stopAndNotify(new IllegalStateException(
+                    "Output format changed after muxer started"));
+                return;
+            }
+
+            try {
+                int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
+                int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
+                mNumTiles = gridRows * gridCols;
+            } catch (NullPointerException | ClassCastException  e) {
+                mNumTiles = 1;
+            }
+
+            // add mMaxImages image tracks of the same format
+            mTrackIndexArray = new int[mMaxImages];
+
+            // set rotation angle
+            if (mRotation > 0) {
+                Log.d(TAG, "setting rotation: " + mRotation);
+                mMuxer.setOrientationHint(mRotation);
+            }
+            for (int i = 0; i < mTrackIndexArray.length; i++) {
+                // mark primary
+                format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
+                mTrackIndexArray[i] = mMuxer.addTrack(format);
+            }
+            mMuxer.start();
+            mMuxerStarted.set(true);
+            processExifData();
+        }
+
+        /**
+         * Upon receiving an output buffer from the encoder (which is one image when
+         * grid is not used, or one tile if grid is used), add that sample to the muxer.
+         */
+        @Override
+        public void onDrainOutputBuffer(
+            @NonNull EncoderBase encoder, @NonNull ByteBuffer byteBuffer) {
+            if (mEncoderStopped) return;
+
+            if (DEBUG) {
+                Log.d(TAG, "onDrainOutputBuffer: " + mOutputIndex);
+            }
+            if (mTrackIndexArray == null) {
+                stopAndNotify(new IllegalStateException(
+                    "Output buffer received before format info"));
+                return;
+            }
+
+            if (mOutputIndex < mMaxImages * mNumTiles) {
+                MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+                info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
+                mMuxer.writeSampleData(
+                    mTrackIndexArray[mOutputIndex / mNumTiles], byteBuffer, info);
+            }
+
+            mOutputIndex++;
+
+            // post EOS if reached max number of images allowed.
+            if (mOutputIndex == mMaxImages * mNumTiles) {
+                stopAndNotify(null);
+            }
+        }
+
+        @Override
+        public void onComplete(@NonNull EncoderBase encoder) {
+            stopAndNotify(null);
+        }
+
+        @Override
+        public void onError(@NonNull EncoderBase encoder, @NonNull MediaCodec.CodecException e) {
+            stopAndNotify(e);
+        }
+
+        private void stopAndNotify(@Nullable Exception error) {
+            if (mEncoderStopped) return;
+
+            mEncoderStopped = true;
+            mResultWaiter.signalResult(error);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static class ResultWaiter {
+        private boolean mDone;
+        private Exception mException;
+
+        synchronized void waitForResult(long timeoutMs) throws Exception {
+            if (timeoutMs < 0) {
+                throw new IllegalArgumentException("timeoutMs is negative");
+            }
+            if (timeoutMs == 0) {
+                while (!mDone) {
+                    try {
+                        wait();
+                    } catch (InterruptedException ex) {}
+                }
+            } else {
+                final long startTimeMs = System.currentTimeMillis();
+                long remainingWaitTimeMs = timeoutMs;
+                // avoid early termination by "spurious" wakeup.
+                while (!mDone && remainingWaitTimeMs > 0) {
+                    try {
+                        wait(remainingWaitTimeMs);
+                    } catch (InterruptedException ex) {}
+                    remainingWaitTimeMs -= (System.currentTimeMillis() - startTimeMs);
+                }
+            }
+            if (!mDone) {
+                mDone = true;
+                mException = new TimeoutException("timed out waiting for result");
+            }
+            if (mException != null) {
+                throw mException;
+            }
+        }
+
+        synchronized void signalResult(@Nullable Exception e) {
+            if (!mDone) {
+                mDone = true;
+                mException = e;
+                notifyAll();
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+        mHandler.postAtFrontOfQueue(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    closeInternal();
+                } catch (Exception e) {
+                    // If the client called stop() properly, any errors would have been
+                    // reported there. We don't want to crash when closing.
+                }
+            }
+        });
+    }
+
+    /*
+     * Gets rotation.
+     */
+    public int getRotation() {
+        return mRotation;
+    }
+
+    /*
+     * Returns true if grid is enabled.
+     */
+    public boolean isGridEnabled() {
+        return mGridEnabled;
+    }
+
+    /*
+     * Gets configured quality.
+     */
+    public int getQuality() {
+        return mQuality;
+    }
+
+    /*
+     * Gets number of maximum images.
+     */
+    public int getMaxImages() {
+        return mMaxImages;
+    }
+
+    /*
+     * Gets index of the primary image.
+     */
+    public int getPrimaryIndex() {
+        return mPrimaryIndex;
+    }
+
+    /*
+     * Gets handler.
+     */
+    public @Nullable Handler getHandler() {
+        return mHandler;
+    }
+
+    /*
+     * Returns true if high bit-depth is enabled.
+     */
+    public boolean isHighBitDepthEnabled() {
+        return mHighBitDepthEnabled;
+    }
+}
\ No newline at end of file
diff --git a/leanback/leanback/api/api_lint.ignore b/leanback/leanback/api/api_lint.ignore
index a72d2ae..a568a48 100644
--- a/leanback/leanback/api/api_lint.ignore
+++ b/leanback/leanback/api/api_lint.ignore
@@ -147,8 +147,6 @@
     Invalid nullability on parameter `view` in method `onViewCreated`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.leanback.widget.GuidedActionEditText#onTouchEvent(android.view.MotionEvent) parameter #0:
     Invalid nullability on parameter `event` in method `onTouchEvent`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.leanback.widget.ShadowOverlayContainer#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 
 
 KotlinOperator: androidx.leanback.widget.ObjectAdapter#get(int):
@@ -1135,6 +1133,8 @@
     Missing nullability on field `TOP_FRACTION` in class `class androidx.leanback.graphics.CompositeDrawable.ChildDrawable`
 MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#PROPERTY_VERTICAL_OFFSET:
     Missing nullability on field `PROPERTY_VERTICAL_OFFSET` in class `class androidx.leanback.graphics.FitWidthBitmapDrawable`
+MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#getBitmap():
     Missing nullability on method `getBitmap` return
 MissingNullability: androidx.leanback.graphics.FitWidthBitmapDrawable#getConstantState():
@@ -2189,6 +2189,8 @@
     Missing nullability on parameter `context` in method `ShadowOverlayContainer`
 MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#ShadowOverlayContainer(android.content.Context, android.util.AttributeSet, int) parameter #1:
     Missing nullability on parameter `attrs` in method `ShadowOverlayContainer`
+MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#getWrappedView():
     Missing nullability on method `getWrappedView` return
 MissingNullability: androidx.leanback.widget.ShadowOverlayContainer#prepareParentForShadow(android.view.ViewGroup) parameter #0:
diff --git a/libraryversions.toml b/libraryversions.toml
index 2978fe7..78ceb4c 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -93,6 +93,7 @@
 PERCENTLAYOUT = "1.1.0-alpha01"
 PREFERENCE = "1.3.0-alpha01"
 PRINT = "1.1.0-beta01"
+PRIVACYSANDBOX_ADS = "1.0.0-alpha01"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
 PRIVACYSANDBOX_TOOLS = "1.0.0-alpha03"
 PRIVACYSANDBOX_UI = "1.0.0-alpha01"
@@ -229,6 +230,7 @@
 PRINT = { group = "androidx.print", atomicGroupVersion = "versions.PRINT" }
 PRIVACYSANDBOX_SDKRUNTIME = { group = "androidx.privacysandbox.sdkruntime", atomicGroupVersion = "versions.PRIVACYSANDBOX_SDKRUNTIME" }
 PRIVACYSANDBOX_TOOLS = { group = "androidx.privacysandbox.tools", atomicGroupVersion = "versions.PRIVACYSANDBOX_TOOLS" }
+PRIVACYSANDBOX_ADS = { group = "androidx.privacysandbox.ads", atomicGroupVersion = "versions.PRIVACYSANDBOX_ADS" }
 PRIVACYSANDBOX_UI = { group = "androidx.privacysandbox.ui", atomicGroupVersion = "versions.PRIVACYSANDBOX_UI" }
 PROFILEINSTALLER = { group = "androidx.profileinstaller", atomicGroupVersion = "versions.PROFILEINSTALLER" }
 RECOMMENDATION = { group = "androidx.recommendation", atomicGroupVersion = "versions.RECOMMENDATION" }
diff --git a/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index b1cf094..e1077a5 100644
--- a/mediarouter/mediarouter/api/current.txt
+++ b/mediarouter/mediarouter/api/current.txt
@@ -172,6 +172,7 @@
     method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
     method public int getConnectionState();
     method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public java.util.Set<java.lang.String!> getDeduplicationIds();
     method public String? getDescription();
     method public int getDeviceType();
     method public android.os.Bundle? getExtras();
@@ -201,6 +202,7 @@
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
     method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeduplicationIds(java.util.Set<java.lang.String!>);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
diff --git a/mediarouter/mediarouter/api/public_plus_experimental_current.txt b/mediarouter/mediarouter/api/public_plus_experimental_current.txt
index b1cf094..e1077a5 100644
--- a/mediarouter/mediarouter/api/public_plus_experimental_current.txt
+++ b/mediarouter/mediarouter/api/public_plus_experimental_current.txt
@@ -172,6 +172,7 @@
     method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
     method public int getConnectionState();
     method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public java.util.Set<java.lang.String!> getDeduplicationIds();
     method public String? getDescription();
     method public int getDeviceType();
     method public android.os.Bundle? getExtras();
@@ -201,6 +202,7 @@
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
     method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeduplicationIds(java.util.Set<java.lang.String!>);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
diff --git a/mediarouter/mediarouter/api/restricted_current.txt b/mediarouter/mediarouter/api/restricted_current.txt
index b1cf094..e1077a5 100644
--- a/mediarouter/mediarouter/api/restricted_current.txt
+++ b/mediarouter/mediarouter/api/restricted_current.txt
@@ -172,6 +172,7 @@
     method public static androidx.mediarouter.media.MediaRouteDescriptor? fromBundle(android.os.Bundle?);
     method public int getConnectionState();
     method public java.util.List<android.content.IntentFilter!> getControlFilters();
+    method public java.util.Set<java.lang.String!> getDeduplicationIds();
     method public String? getDescription();
     method public int getDeviceType();
     method public android.os.Bundle? getExtras();
@@ -201,6 +202,7 @@
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setCanDisconnect(boolean);
     method @Deprecated public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnecting(boolean);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setConnectionState(int);
+    method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeduplicationIds(java.util.Set<java.lang.String!>);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDescription(String?);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setDeviceType(int);
     method public androidx.mediarouter.media.MediaRouteDescriptor.Builder setEnabled(boolean);
diff --git a/mediarouter/mediarouter/build.gradle b/mediarouter/mediarouter/build.gradle
index fcaf016..e9e3b8d 100644
--- a/mediarouter/mediarouter/build.gradle
+++ b/mediarouter/mediarouter/build.gradle
@@ -25,11 +25,12 @@
     api("androidx.media:media:1.4.1")
     api(libs.guavaListenableFuture)
 
-    implementation("androidx.core:core:1.6.0")
+    implementation("androidx.core:core:1.8.0")
     implementation("androidx.appcompat:appcompat:1.1.0")
     implementation("androidx.palette:palette:1.0.0")
     implementation("androidx.recyclerview:recyclerview:1.1.0")
     implementation("androidx.appcompat:appcompat-resources:1.2.0")
+    implementation "androidx.annotation:annotation-experimental:1.3.0"
 
     testImplementation(libs.junit)
     testImplementation(libs.testCore)
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java
index ae769ed..5c524fd 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2UtilsTest.java
@@ -16,11 +16,17 @@
 
 package androidx.mediarouter.media;
 
+import static androidx.mediarouter.media.MediaRouter2Utils.KEY_CONTROL_FILTERS;
+import static androidx.mediarouter.media.MediaRouter2Utils.KEY_DEVICE_TYPE;
+import static androidx.mediarouter.media.MediaRouter2Utils.KEY_EXTRAS;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import android.media.MediaRoute2Info;
 import android.os.Build;
+import android.os.Bundle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
@@ -29,6 +35,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.HashSet;
+
 /** Test for {@link MediaRouter2Utils}. */
 @SmallTest
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -60,4 +69,45 @@
                         .build();
         assertNull(MediaRouter2Utils.toFwkMediaRoute2Info(descriptorWithEmptyName));
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+    @Test
+    public void toFwkMediaRoute2Info_withDeduplicationIds() {
+        HashSet<String> dedupIds = new HashSet<>();
+        dedupIds.add("dedup_id1");
+        dedupIds.add("dedup_id2");
+        MediaRouteDescriptor descriptor =
+                new MediaRouteDescriptor.Builder(
+                                FAKE_MEDIA_ROUTE_DESCRIPTOR_ID, FAKE_MEDIA_ROUTE_DESCRIPTOR_NAME)
+                        .setDeduplicationIds(dedupIds)
+                        .build();
+        assertTrue(
+                MediaRouter2Utils.toFwkMediaRoute2Info(descriptor)
+                        .getDeduplicationIds()
+                        .equals(dedupIds));
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+    @Test
+    public void toMediaRouteDescriptor_withDeduplicationIds() {
+        HashSet<String> dedupIds = new HashSet<>();
+        dedupIds.add("dedup_id1");
+        dedupIds.add("dedup_id2");
+        // Extras needed to make toMediaRouteDescriptor not return null.
+        Bundle extras = new Bundle();
+        extras.putBundle(KEY_EXTRAS, new Bundle());
+        extras.putInt(KEY_DEVICE_TYPE, MediaRouter.RouteInfo.DEVICE_TYPE_UNKNOWN);
+        extras.putParcelableArrayList(KEY_CONTROL_FILTERS, new ArrayList<>());
+        MediaRoute2Info routeInfo =
+                new MediaRoute2Info.Builder(
+                                FAKE_MEDIA_ROUTE_DESCRIPTOR_ID, FAKE_MEDIA_ROUTE_DESCRIPTOR_NAME)
+                        .addFeature(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)
+                        .setDeduplicationIds(dedupIds)
+                        .setExtras(extras)
+                        .build();
+        assertTrue(
+                MediaRouter2Utils.toMediaRouteDescriptor(routeInfo)
+                        .getDeduplicationIds()
+                        .equals(dedupIds));
+    }
 }
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 e77626e..73a1ddd 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -46,7 +46,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
 import androidx.mediarouter.R;
 import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor;
 import androidx.mediarouter.media.MediaRouter.ControlRequestCallback;
@@ -72,7 +74,7 @@
     final Callback mCallback;
     final Map<MediaRouter2.RoutingController, GroupRouteController> mControllerMap =
             new ArrayMap<>();
-    private final MediaRouter2.RouteCallback mRouteCallback = new RouteCallback();
+    private final MediaRouter2.RouteCallback mRouteCallback;
     private final MediaRouter2.TransferCallback mTransferCallback = new TransferCallback();
     private final MediaRouter2.ControllerCallback mControllerCallback = new ControllerCallback();
     private final Handler mHandler;
@@ -81,6 +83,8 @@
     private List<MediaRoute2Info> mRoutes = new ArrayList<>();
     private Map<String, String> mRouteIdToOriginalRouteIdMap = new ArrayMap<>();
 
+    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
+    @SuppressWarnings({"SyntheticAccessor"})
     MediaRoute2Provider(@NonNull Context context, @NonNull Callback callback) {
         super(context);
         mMediaRouter2 = MediaRouter2.getInstance(context);
@@ -88,6 +92,12 @@
 
         mHandler = new Handler(Looper.getMainLooper());
         mHandlerExecutor = mHandler::post;
+
+        if (BuildCompat.isAtLeastU()) {
+            mRouteCallback = new RouteCallbackUpsideDownCake();
+        } else {
+            mRouteCallback = new RouteCallback();
+        }
     }
 
     @Override
@@ -380,6 +390,14 @@
         }
     }
 
+    private class RouteCallbackUpsideDownCake extends MediaRouter2.RouteCallback {
+
+        @Override
+        public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
+            refreshRoutes();
+        }
+    }
+
     private class TransferCallback extends MediaRouter2.TransferCallback {
         TransferCallback() {}
 
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java
index 314a72f..bc16e9f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteDescriptor.java
@@ -31,7 +31,9 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Describes the properties of a route.
@@ -65,6 +67,7 @@
     static final String KEY_SETTINGS_INTENT = "settingsIntent";
     static final String KEY_MIN_CLIENT_VERSION = "minClientVersion";
     static final String KEY_MAX_CLIENT_VERSION = "maxClientVersion";
+    static final String KEY_DEDUPLICATION_IDS = "deduplicationIds";
 
     final Bundle mBundle;
     List<String> mGroupMemberIds;
@@ -299,6 +302,20 @@
     }
 
     /**
+     * Gets the route's deduplication ids.
+     *
+     * <p>Two routes are considered to come from the same receiver device if any of their respective
+     * deduplication ids match.
+     */
+    @NonNull
+    public Set<String> getDeduplicationIds() {
+        ArrayList<String> deduplicationIds = mBundle.getStringArrayList(KEY_DEDUPLICATION_IDS);
+        return deduplicationIds != null
+                ? Collections.unmodifiableSet(new HashSet<>(deduplicationIds))
+                : Collections.emptySet();
+    }
+
+    /**
      * Gets the route's presentation display id, or -1 if none.
      */
     public int getPresentationDisplayId() {
@@ -767,6 +784,21 @@
         }
 
         /**
+         * Sets the route's deduplication ids.
+         *
+         * <p>Two routes are considered to come from the same receiver device if any of their
+         * respective deduplication ids match.
+         *
+         * @param deduplicationIds A set of strings that uniquely identify the receiver device that
+         *     backs this route.
+         */
+        @NonNull
+        public Builder setDeduplicationIds(@NonNull Set<String> deduplicationIds) {
+            mBundle.putStringArrayList(KEY_DEDUPLICATION_IDS, new ArrayList<>(deduplicationIds));
+            return this;
+        }
+
+        /**
          * Sets the route's presentation display id, or -1 if none.
          */
         @NonNull
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
index 5514799..8ac3e5b 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
@@ -35,9 +35,12 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 
+import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -65,6 +68,7 @@
 
     private MediaRouter2Utils() {}
 
+    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
     @Nullable
     public static MediaRoute2Info toFwkMediaRoute2Info(@Nullable MediaRouteDescriptor descriptor) {
         if (descriptor == null) {
@@ -88,6 +92,10 @@
                 //.setClientPackageName(clientMap.get(device.getDeviceId()))
                 ;
 
+        if (BuildCompat.isAtLeastU()) {
+            Api34Impl.setDeduplicationIds(builder, descriptor.getDeduplicationIds());
+        }
+
         switch (descriptor.getDeviceType()) {
             case DEVICE_TYPE_TV:
                 builder.addFeature(FEATURE_REMOTE_VIDEO_PLAYBACK);
@@ -118,6 +126,7 @@
         return builder.build();
     }
 
+    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
     @Nullable
     public static MediaRouteDescriptor toMediaRouteDescriptor(
             @Nullable MediaRoute2Info fwkMediaRoute2Info) {
@@ -135,6 +144,10 @@
                 .setEnabled(true)
                 .setCanDisconnect(false);
 
+        if (BuildCompat.isAtLeastU()) {
+            builder.setDeduplicationIds(Api34Impl.getDeduplicationIds(fwkMediaRoute2Info));
+        }
+
         CharSequence description = fwkMediaRoute2Info.getDescription();
         if (description != null) {
             builder.setDescription(description.toString());
@@ -276,4 +289,19 @@
         }
         return routeFeature;
     }
+
+    @RequiresApi(api = 34)
+    private static final class Api34Impl {
+
+        @DoNotInline
+        public static void setDeduplicationIds(
+                MediaRoute2Info.Builder builder, Set<String> deduplicationIds) {
+            builder.setDeduplicationIds(deduplicationIds);
+        }
+
+        @DoNotInline
+        public static Set<String> getDeduplicationIds(MediaRoute2Info fwkMediaRoute2Info) {
+            return fwkMediaRoute2Info.getDeduplicationIds();
+        }
+    }
 }
diff --git a/privacysandbox/ads/OWNERS b/privacysandbox/ads/OWNERS
new file mode 100644
index 0000000..67d0de6
--- /dev/null
+++ b/privacysandbox/ads/OWNERS
@@ -0,0 +1,3 @@
+# Please keep this list alphabetically sorted
[email protected]
[email protected]
diff --git a/privacysandbox/ads/ads-adservices-java/api/current.txt b/privacysandbox/ads/ads-adservices-java/api/current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices-java/api/public_plus_experimental_current.txt b/privacysandbox/ads/ads-adservices-java/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/public_plus_experimental_current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/privacysandbox/ads/ads-adservices-java/api/res-current.txt
similarity index 100%
copy from webkit/webkit/api/res-1.6.0-beta02.txt
copy to privacysandbox/ads/ads-adservices-java/api/res-current.txt
diff --git a/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt b/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
new file mode 100644
index 0000000..af73f43
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core-ktx:1.8.0")
+    api("androidx.annotation:annotation:1.2.0")
+
+    // To use CallbackToFutureAdapter
+    implementation "androidx.concurrent:concurrent-futures:1.1.0"
+    implementation(libs.guavaAndroid)
+    api(libs.guavaListenableFuture)
+    implementation project(path: ':privacysandbox:ads:ads-adservices')
+
+    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
+    androidTestImplementation project(path: ':privacysandbox:ads:ads-adservices')
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+
+    androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+}
+
+android {
+    compileSdk = 33
+    compileSdkExtension = 4
+    namespace "androidx.privacysandbox.ads.adservices.java"
+}
+
+androidx {
+    name = "androidx.privacysandbox.ads:ads-adservices-java"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2022"
+    description = "write Java code to call PP APIs."
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..faff43e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" />
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" />
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" />
+    <application>
+        <property android:name="android.adservices.AD_SERVICES_CONFIG"
+            android:resource="@xml/ad_services_config" />
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
new file mode 100644
index 0000000..bf0a81b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.adid.AdId
+import androidx.test.core.app.ApplicationProvider
+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.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdIdManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = Mockito.spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(AdIdManagerFutures.from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAdIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adIdManager = mockAdIdManager(mContext)
+        setupResponse(adIdManager)
+        val managerCompat = AdIdManagerFutures.from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AdId> = managerCompat!!.getAdIdAsync()
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        Mockito.verify(adIdManager).getAdId(ArgumentMatchers.any(), ArgumentMatchers.any())
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+            val adIdManager = Mockito.mock(android.adservices.adid.AdIdManager::class.java)
+            Mockito.`when`(spyContext.getSystemService(
+                android.adservices.adid.AdIdManager::class.java)).thenReturn(adIdManager)
+            return adIdManager
+        }
+
+        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            Mockito.doAnswer(answer)
+                .`when`(adIdManager).getAdId(
+                    ArgumentMatchers.any(),
+                    ArgumentMatchers.any()
+                )
+        }
+
+        private fun verifyResponse(adId: androidx.privacysandbox.ads.adservices.adid.AdId) {
+            Assert.assertEquals("1234", adId.adId)
+            Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
new file mode 100644
index 0000000..ff87882
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
@@ -0,0 +1,236 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adselection
+
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+import androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest
+import androidx.test.core.app.ApplicationProvider
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion.from
+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.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdSelectionManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdSelectionOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testSelectAds() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AdSelectionOutcome> =
+            managerCompat!!.selectAdsAsync(adSelectionConfig)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.AdSelectionConfig::class.java)
+        verify(adSelectionManager).selectAds(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testReportImpression() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = from(mContext)
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+
+        // Actually invoke the compat code.
+        managerCompat!!.reportImpressionAsync(reportImpressionRequest).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.ReportImpressionRequest::class.java)
+        verify(adSelectionManager).reportImpression(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyReportImpressionRequest(captor.value)
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private const val adSelectionId = 1234L
+        private const val adId = "1234"
+        private val seller: AdTechIdentifier = AdTechIdentifier(adId)
+        private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+        private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+        private const val adSelectionSignalsStr = "adSelSignals"
+        private val adSelectionSignals: AdSelectionSignals =
+            AdSelectionSignals(adSelectionSignalsStr)
+        private const val sellerSignalsStr = "sellerSignals"
+        private val sellerSignals: AdSelectionSignals = AdSelectionSignals(sellerSignalsStr)
+        private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+            mutableMapOf(Pair(seller, sellerSignals))
+        private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+        private val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+
+        // Response.
+        private val renderUri = Uri.parse("render-uri.com")
+
+        private fun mockAdSelectionManager(
+            spyContext: Context
+        ): android.adservices.adselection.AdSelectionManager {
+            val adSelectionManager =
+                mock(android.adservices.adselection.AdSelectionManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.adselection.AdSelectionManager::class.java))
+                .thenReturn(adSelectionManager)
+            return adSelectionManager
+        }
+
+        private fun setupAdSelectionResponse(
+            adSelectionManager: android.adservices.adselection.AdSelectionManager
+        ) {
+            // Set up the response that AdSelectionManager will return when the compat code calls
+            // it.
+            val response = android.adservices.adselection.AdSelectionOutcome.Builder()
+                .setAdSelectionId(adSelectionId)
+                .setRenderUri(renderUri)
+                .build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<
+                    android.adservices.adselection.AdSelectionOutcome, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adSelectionManager).selectAds(
+                    any(),
+                    any(),
+                    any()
+                )
+
+            val answer2 = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer2).`when`(adSelectionManager).reportImpression(any(), any(), any())
+        }
+
+        private fun verifyRequest(request: android.adservices.adselection.AdSelectionConfig) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = getPlatformAdSelectionConfig()
+
+            Assert.assertEquals(expectedRequest, request)
+        }
+
+        private fun verifyResponse(
+            outcome: androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+        ) {
+            val expectedOutcome =
+                androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome(
+                    adSelectionId,
+                    renderUri)
+            Assert.assertEquals(expectedOutcome, outcome)
+        }
+
+        private fun getPlatformAdSelectionConfig():
+            android.adservices.adselection.AdSelectionConfig {
+            val adTechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adId)
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(adSelectionSignalsStr))
+                .setCustomAudienceBuyers(listOf(adTechIdentifier))
+                .setDecisionLogicUri(decisionLogicUri)
+                .setPerBuyerSignals(mutableMapOf(Pair(
+                    adTechIdentifier,
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))))
+                .setSeller(adTechIdentifier)
+                .setSellerSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))
+                .setTrustedScoringSignalsUri(trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun verifyReportImpressionRequest(
+            request: android.adservices.adselection.ReportImpressionRequest
+        ) {
+            val expectedRequest = android.adservices.adselection.ReportImpressionRequest(
+                adSelectionId,
+                getPlatformAdSelectionConfig())
+            Assert.assertEquals(expectedRequest.adSelectionId, request.adSelectionId)
+            Assert.assertEquals(expectedRequest.adSelectionConfig, request.adSelectionConfig)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
new file mode 100644
index 0000000..a558f76
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.appsetid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetId
+import androidx.test.core.app.ApplicationProvider
+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 com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AppSetIdManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAppSetIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AppSetIdManagerFutures.from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAppSetIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val appSetIdManager = mockAppSetIdManager(mContext)
+        setupResponse(appSetIdManager)
+        val managerCompat = AppSetIdManagerFutures.from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AppSetId> = managerCompat!!.getAppSetIdAsync()
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        verify(appSetIdManager).getAppSetId(any(), any())
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAppSetIdManager(
+            spyContext: Context
+        ): android.adservices.appsetid.AppSetIdManager {
+            val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.appsetid.AppSetIdManager::class.java))
+                .thenReturn(appSetIdManager)
+            return appSetIdManager
+        }
+
+        private fun setupResponse(appSetIdManager: android.adservices.appsetid.AppSetIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val appSetId = android.adservices.appsetid.AppSetId(
+                "1234",
+                android.adservices.appsetid.AppSetId.SCOPE_APP)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.appsetid.AppSetId, Exception>>(1)
+                receiver.onResult(appSetId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(appSetIdManager).getAppSetId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(appSetId: AppSetId) {
+            Assert.assertEquals("1234", appSetId.id)
+            Assert.assertEquals(AppSetId.SCOPE_APP, appSetId.scope)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
new file mode 100644
index 0000000..a2fc696
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.customaudience
+
+import android.adservices.customaudience.CustomAudienceManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudience
+import androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion.from
+import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class CustomAudienceManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testJoinCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val customAudience = CustomAudience.Builder(buyer, name, uri, uri, ads)
+            .setActivationTime(Instant.now())
+            .setExpirationTime(Instant.now())
+            .setUserBiddingSignals(userBiddingSignals)
+            .setTrustedBiddingData(trustedBiddingSignals)
+            .build()
+        val request = JoinCustomAudienceRequest(customAudience)
+        managerCompat!!.joinCustomAudienceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.JoinCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).joinCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyJoinCustomAudienceRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testLeaveCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val request = LeaveCustomAudienceRequest(buyer, name)
+        managerCompat!!.leaveCustomAudienceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.LeaveCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).leaveCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyLeaveCustomAudienceRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val uri: Uri = Uri.parse("abc.com")
+        private const val adtech = "1234"
+        private val buyer: AdTechIdentifier = AdTechIdentifier(adtech)
+        private const val name: String = "abc"
+        private const val signals = "signals"
+        private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals(signals)
+        private val keys: List<String> = listOf("key1", "key2")
+        private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+        private const val metadata = "metadata"
+        private val ads: List<AdData> = listOf(AdData(uri, metadata))
+
+        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+            val customAudienceManager = mock(CustomAudienceManager::class.java)
+            `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
+                .thenReturn(customAudienceManager)
+            return customAudienceManager
+        }
+
+        private fun setupResponse(customAudienceManager: CustomAudienceManager) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer).`when`(customAudienceManager).joinCustomAudience(any(), any(), any())
+            doAnswer(answer).`when`(customAudienceManager).leaveCustomAudience(any(), any(), any())
+        }
+
+        private fun verifyJoinCustomAudienceRequest(
+            joinCustomAudienceRequest: android.adservices.customaudience.JoinCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+            val userBiddingSignals =
+                android.adservices.common.AdSelectionSignals.fromString(signals)
+            val trustedBiddingSignals =
+                android.adservices.customaudience.TrustedBiddingData.Builder()
+                    .setTrustedBiddingKeys(keys)
+                    .setTrustedBiddingUri(uri)
+                    .build()
+            val customAudience = android.adservices.customaudience.CustomAudience.Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setBiddingLogicUri(uri)
+                .setDailyUpdateUri(uri)
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .setAds(listOf(android.adservices.common.AdData.Builder()
+                    .setRenderUri(uri)
+                    .setMetadata(metadata)
+                    .build()))
+                .build()
+
+            val expectedRequest =
+                android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                    .setCustomAudience(customAudience)
+                    .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == joinCustomAudienceRequest).isTrue()
+        }
+
+        private fun verifyLeaveCustomAudienceRequest(
+            leaveCustomAudienceRequest: android.adservices.customaudience.LeaveCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+
+            val expectedRequest = android.adservices.customaudience.LeaveCustomAudienceRequest
+                .Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == leaveCustomAudienceRequest).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
new file mode 100644
index 0000000..8d5d99a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -0,0 +1,168 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.List;
+
+public class TestUtil {
+    private Instrumentation mInstrumentation;
+    private String mTag;
+    // Used to get the package name. Copied over from com.android.adservices.AdServicesCommon
+    private static final String TOPICS_SERVICE_NAME = "android.adservices.TOPICS_SERVICE";
+    // The JobId of the Epoch Computation.
+    private static final int EPOCH_JOB_ID = 2;
+
+    public TestUtil(Instrumentation instrumentation, String tag) {
+        mInstrumentation = instrumentation;
+        mTag = tag;
+    }
+    // Run shell command.
+    private void runShellCommand(String command) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                mInstrumentation.getUiAutomation().executeShellCommand(command);
+            }
+        }
+    }
+    public void overrideKillSwitches(boolean override) {
+        if (override) {
+            runShellCommand("setprop debug.adservices.global_kill_switch " + false);
+            runShellCommand("setprop debug.adservices.topics_kill_switch " + false);
+        } else {
+            runShellCommand("setprop debug.adservices.global_kill_switch " + null);
+            runShellCommand("setprop debug.adservices.topics_kill_switch " + null);
+        }
+    }
+
+    public void enableEnrollmentCheck(boolean enable) {
+        runShellCommand(
+                "setprop debug.adservices.disable_topics_enrollment_check " + enable);
+    }
+
+    // Override the Epoch Period to shorten the Epoch Length in the test.
+    public void overrideEpochPeriod(long overrideEpochPeriod) {
+        runShellCommand(
+                "setprop debug.adservices.topics_epoch_job_period_ms " + overrideEpochPeriod);
+    }
+
+    // Override the Percentage For Random Topic in the test.
+    public void overridePercentageForRandomTopic(long overridePercentage) {
+        runShellCommand(
+                "setprop debug.adservices.topics_percentage_for_random_topics "
+                        + overridePercentage);
+    }
+
+    /** Forces JobScheduler to run the Epoch Computation job */
+    public void forceEpochComputationJob() {
+        runShellCommand(
+                "cmd jobscheduler run -f" + " " + getAdServicesPackageName() + " " + EPOCH_JOB_ID);
+    }
+
+    public void overrideConsentManagerDebugMode(boolean override) {
+        String overrideStr = override ? "true" : "null";
+        runShellCommand("setprop debug.adservices.consent_manager_debug_mode " + overrideStr);
+    }
+
+    public void overrideAllowlists(boolean override) {
+        String overrideStr = override ? "*" : "null";
+        runShellCommand("device_config put adservices ppapi_app_allow_list " + overrideStr);
+        runShellCommand("device_config put adservices ppapi_app_signature_allow_list "
+                + overrideStr);
+        runShellCommand(
+                "device_config put adservices web_context_client_allow_list " + overrideStr);
+    }
+
+    public void overrideAdIdKillSwitch(boolean override) {
+        if (override) {
+            runShellCommand("setprop debug.adservices.adid_kill_switch " + false);
+        } else {
+            runShellCommand("setprop debug.adservices.adid_kill_switch " + null);
+        }
+    }
+
+    // Override measurement related kill switch to ignore the effect of actual PH values.
+    // If isOverride = true, override measurement related kill switch to OFF to allow adservices
+    // If isOverride = false, override measurement related kill switch to meaningless value so that
+    // PhFlags will use the default value.
+    public void overrideMeasurementKillSwitches(boolean isOverride) {
+        String overrideString = isOverride ? "false" : "null";
+        runShellCommand("setprop debug.adservices.global_kill_switch " + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_kill_switch " + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_source_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_trigger_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_web_source_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_web_trigger_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_delete_registrations_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_status_kill_switch "
+                + overrideString);
+    }
+
+    // Override the flag to disable Measurement enrollment check. Setting to 1 disables enforcement.
+    public void overrideDisableMeasurementEnrollmentCheck(String val) {
+        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check " + val);
+    }
+
+    public void resetOverrideDisableMeasurementEnrollmentCheck() {
+        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check null");
+    }
+
+    @SuppressWarnings("deprecation")
+    // Used to get the package name. Copied over from com.android.adservices.AndroidServiceBinder
+    public String getAdServicesPackageName() {
+        final Intent intent = new Intent(TOPICS_SERVICE_NAME);
+        final List<ResolveInfo> resolveInfos = ApplicationProvider.getApplicationContext()
+                .getPackageManager()
+                .queryIntentServices(intent, PackageManager.MATCH_SYSTEM_ONLY);
+
+        if (resolveInfos == null || resolveInfos.isEmpty()) {
+            Log.e(mTag, "Failed to find resolveInfo for adServices service. Intent action: "
+                            + TOPICS_SERVICE_NAME);
+            return null;
+        }
+
+        if (resolveInfos.size() > 1) {
+            String str = String.format(
+                    "Found multiple services (%1$s) for the same intent action (%2$s)",
+                    TOPICS_SERVICE_NAME, resolveInfos);
+            Log.e(mTag, str);
+            return null;
+        }
+
+        final ServiceInfo serviceInfo = resolveInfos.get(0).serviceInfo;
+        if (serviceInfo == null) {
+            Log.e(mTag, "Failed to find serviceInfo for adServices service");
+            return null;
+        }
+
+        return serviceInfo.packageName;
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
new file mode 100644
index 0000000..f7e3cd44
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend.adid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.privacysandbox.ads.adservices.adid.AdId;
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class AdIdManagerTest {
+    private static final String TAG = "AdIdManagerTest";
+    private TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    @Before
+    public void setup() throws Exception {
+        mTestUtil.overrideAdIdKillSwitch(true);
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideAllowlists(true);
+    }
+
+    @After
+    public void teardown() {
+        mTestUtil.overrideAdIdKillSwitch(false);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.overrideAllowlists(false);
+    }
+
+    @Test
+    public void testAdId() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        AdIdManagerFutures adIdManager =
+                AdIdManagerFutures.from(ApplicationProvider.getApplicationContext());
+        AdId adId = adIdManager.getAdIdAsync().get();
+        assertThat(adId.getAdId()).isNotEmpty();
+        assertThat(adId.isLimitAdTrackingEnabled()).isNotNull();
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
new file mode 100644
index 0000000..2cb7889
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend.measurement;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.Uri;
+
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures;
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest;
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceParams;
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest;
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams;
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+// TODO: Consider refactoring so that we're not duplicating code.
+public class MeasurementManagerTest {
+    private static final String TAG = "MeasurementManagerTest";
+    TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    /* Note: The source and trigger registration used here must match one of those in
+       {@link PreEnrolledAdTechForTest}.
+    */
+    private static final Uri SOURCE_REGISTRATION_URI = Uri.parse("https://test.com/source");
+    private static final Uri TRIGGER_REGISTRATION_URI = Uri.parse("https://test.com/trigger");
+    private static final Uri DESTINATION = Uri.parse("http://trigger-origin.com");
+    private static final Uri OS_DESTINATION = Uri.parse("android-app://com.os.destination");
+    private static final Uri WEB_DESTINATION = Uri.parse("http://web-destination.com");
+    private static final Uri ORIGIN_URI = Uri.parse("https://sample.example1.com");
+    private static final Uri DOMAIN_URI = Uri.parse("https://example2.com");
+
+    private MeasurementManagerFutures mMeasurementManager;
+
+    @Before
+    public void setup() {
+        // To grant access to all pp api app
+        mTestUtil.overrideAllowlists(true);
+        // We need to turn the Consent Manager into debug mode
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideMeasurementKillSwitches(true);
+        mTestUtil.overrideDisableMeasurementEnrollmentCheck("1");
+        mMeasurementManager =
+                MeasurementManagerFutures.from(ApplicationProvider.getApplicationContext());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTestUtil.overrideAllowlists(false);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.resetOverrideDisableMeasurementEnrollmentCheck();
+        mTestUtil.overrideMeasurementKillSwitches(false);
+        mTestUtil.overrideDisableMeasurementEnrollmentCheck("0");
+        // Cool-off rate limiter
+        TimeUnit.SECONDS.sleep(1);
+    }
+
+    @Test
+    public void testRegisterSource_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        assertThat(mMeasurementManager.registerSourceAsync(
+                SOURCE_REGISTRATION_URI,
+                /* inputEvent= */ null).get())
+                .isNotNull();
+    }
+
+    @Test
+    public void testRegisterTrigger_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        assertThat(mMeasurementManager.registerTriggerAsync(TRIGGER_REGISTRATION_URI).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void registerWebSource_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        WebSourceParams webSourceParams =
+                new WebSourceParams(SOURCE_REGISTRATION_URI, false);
+
+        WebSourceRegistrationRequest webSourceRegistrationRequest =
+                new WebSourceRegistrationRequest(
+                        Collections.singletonList(webSourceParams),
+                        SOURCE_REGISTRATION_URI,
+                        /* inputEvent= */ null,
+                        OS_DESTINATION,
+                        WEB_DESTINATION,
+                        /* verifiedDestination= */ null);
+
+        assertThat(mMeasurementManager.registerWebSourceAsync(webSourceRegistrationRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void registerWebTrigger_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        WebTriggerParams webTriggerParams =
+                new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
+        WebTriggerRegistrationRequest webTriggerRegistrationRequest =
+                new WebTriggerRegistrationRequest(
+                        Collections.singletonList(webTriggerParams),
+                        DESTINATION);
+
+        assertThat(mMeasurementManager.registerWebTriggerAsync(webTriggerRegistrationRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withNoRange_withCallback_NoErrors()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .build();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withEmptyLists_withRange_withCallback_NoErrors()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .setStart(Instant.ofEpochMilli(0))
+                        .setEnd(Instant.now())
+                        .build();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withInvalidArguments_withCallback_hasError()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .setStart(Instant.now().plusMillis(1000))
+                        .setEnd(Instant.now())
+                        .build();
+        Exception exception = assertThrows(
+                ExecutionException.class,
+                () ->
+                mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get());
+        assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testMeasurementApiStatus_returnResultStatus() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        int result = mMeasurementManager.getMeasurementApiStatusAsync().get();
+        assertThat(result).isEqualTo(1);
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
new file mode 100644
index 0000000..247232a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.endtoend.topics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures;
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest;
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse;
+import androidx.privacysandbox.ads.adservices.topics.Topic;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+// TODO: Consider refactoring so that we're not duplicating code.
+public class TopicsManagerTest {
+    private static final String TAG = "TopicsManagerTest";
+    TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    // Override the Epoch Job Period to this value to speed up the epoch computation.
+    private static final long TEST_EPOCH_JOB_PERIOD_MS = 3000;
+
+    // Default Epoch Period.
+    private static final long TOPICS_EPOCH_JOB_PERIOD_MS = 7 * 86_400_000; // 7 days.
+
+    // Use 0 percent for random topic in the test so that we can verify the returned topic.
+    private static final int TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 0;
+    private static final int TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 5;
+
+    @Before
+    public void setup() throws Exception {
+        mTestUtil.overrideKillSwitches(true);
+        // We need to skip 3 epochs so that if there is any usage from other test runs, it will
+        // not be used for epoch retrieval.
+        Thread.sleep(3 * TEST_EPOCH_JOB_PERIOD_MS);
+
+        mTestUtil.overrideEpochPeriod(TEST_EPOCH_JOB_PERIOD_MS);
+        // We need to turn off random topic so that we can verify the returned topic.
+        mTestUtil.overridePercentageForRandomTopic(TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC);
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideAllowlists(true);
+        // TODO: Remove this override.
+        mTestUtil.enableEnrollmentCheck(true);
+    }
+
+    @After
+    public void teardown() {
+        mTestUtil.overrideKillSwitches(false);
+        mTestUtil.overrideEpochPeriod(TOPICS_EPOCH_JOB_PERIOD_MS);
+        mTestUtil.overridePercentageForRandomTopic(TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.overrideAllowlists(false);
+        mTestUtil.enableEnrollmentCheck(false);
+    }
+
+    @Test
+    public void testTopicsManager_runClassifier() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        TopicsManagerFutures topicsManager =
+                TopicsManagerFutures.from(ApplicationProvider.getApplicationContext());
+        GetTopicsRequest request = new GetTopicsRequest.Builder()
+                .setSdkName("sdk1")
+                .setShouldRecordObservation(true)
+                .build();
+        GetTopicsResponse response = topicsManager.getTopicsAsync(request).get();
+
+        // At beginning, Sdk1 receives no topic.
+        assertThat(response.getTopics().isEmpty());
+
+        // Now force the Epoch Computation Job. This should be done in the same epoch for
+        // callersCanLearnMap to have the entry for processing.
+        mTestUtil.forceEpochComputationJob();
+
+        // Wait to the next epoch. We will not need to do this after we implement the fix in
+        // go/rb-topics-epoch-scheduling
+        Thread.sleep(TEST_EPOCH_JOB_PERIOD_MS);
+
+        // Since the sdk1 called the Topics API in the previous Epoch, it should receive some topic.
+        response = topicsManager.getTopicsAsync(request).get();
+        assertThat(response.getTopics()).isNotEmpty();
+
+        // Top 5 classifications for empty string with v2 model are [10230, 10253, 10227, 10250,
+        // 10257]. This is computed by running the model on the device for empty string.
+        // These 5 classification topics will become top 5 topics of the epoch since there is
+        // no other apps calling Topics API.
+        // The app will be assigned one random topic from one of these 5 topics.
+        assertThat(response.getTopics()).hasSize(1);
+
+        Topic topic = response.getTopics().get(0);
+
+        // topic is one of the 5 classification topics of the Test App.
+        assertThat(topic.getTopicId()).isIn(Arrays.asList(10230, 10253, 10227, 10250, 10257));
+
+        assertThat(topic.getModelVersion()).isAtLeast(1L);
+        assertThat(topic.getTaxonomyVersion()).isAtLeast(1L);
+
+        // Sdk 2 did not call getTopics API. So it should not receive any topic.
+        GetTopicsResponse response2 = topicsManager.getTopicsAsync(
+                new GetTopicsRequest.Builder()
+                        .setSdkName("sdk2")
+                        .build()).get();
+        assertThat(response2.getTopics()).isEmpty();
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
new file mode 100644
index 0000000..e4bdbb1
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFuturesTest.kt
@@ -0,0 +1,299 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.measurement
+
+import android.adservices.measurement.MeasurementManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion.from
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceParams
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest
+import androidx.test.core.app.ApplicationProvider
+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 java.time.Instant
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class MeasurementManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testMeasurementOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testDeleteRegistrationsAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+
+        // Set up the request.
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).deleteRegistrations(any(), any(), any())
+
+        // Actually invoke the compat code.
+        val request = DeletionRequest(
+            DeletionRequest.DELETION_MODE_ALL,
+            DeletionRequest.MATCH_BEHAVIOR_DELETE,
+            Instant.now(),
+            Instant.now(),
+            listOf(uri1),
+            listOf(uri1))
+
+        managerCompat!!.deleteRegistrationsAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.measurement.DeletionRequest::class.java
+        )
+        verify(measurementManager).deleteRegistrations(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyDeletionRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterSourceAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val inputEvent = mock(InputEvent::class.java)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerSource(any(), any(), any(), any())
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerSourceAsync(uri1, inputEvent).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
+        verify(measurementManager).registerSource(
+            captor1.capture(),
+            captor2.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value == uri1)
+        assertThat(captor2.value == inputEvent)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterTriggerAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerTrigger(any(), any(), any())
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerTriggerAsync(uri1).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        verify(measurementManager).registerTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value == uri1)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebSourceAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebSource(any(), any(), any())
+
+        val request = WebSourceRegistrationRequest.Builder(
+            listOf(WebSourceParams(uri2, false)), uri1)
+            .setAppDestination(uri1)
+            .build()
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerWebSourceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebSourceRegistrationRequest::class.java)
+        verify(measurementManager).registerWebSource(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.topOriginUri == uri1)
+        assertThat(actualRequest.sourceParams.size == 1)
+        assertThat(actualRequest.sourceParams[0].registrationUri == uri2)
+        assertThat(!actualRequest.sourceParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebTriggerAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebTrigger(any(), any(), any())
+
+        val request = WebTriggerRegistrationRequest(listOf(WebTriggerParams(uri1, false)), uri2)
+
+        // Actually invoke the compat code.
+        managerCompat!!.registerWebTriggerAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebTriggerRegistrationRequest::class.java)
+        verify(measurementManager).registerWebTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.destination == uri2)
+        assertThat(actualRequest.triggerParams.size == 1)
+        assertThat(actualRequest.triggerParams[0].registrationUri == uri1)
+        assertThat(!actualRequest.triggerParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testMeasurementApiStatusAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = from(mContext)
+        val state = MeasurementManager.MEASUREMENT_API_STATE_DISABLED
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+            receiver.onResult(state)
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Actually invoke the compat code.
+        val result = managerCompat!!.getMeasurementApiStatusAsync()
+        result.get()
+
+        // Verify that the compat code was invoked correctly.
+        verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Verify that the result.
+        assertThat(result.get() == state)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+
+        private val uri1: Uri = Uri.parse("www.abc.com")
+        private val uri2: Uri = Uri.parse("http://www.xyz.com")
+        private lateinit var mContext: Context
+
+        private fun mockMeasurementManager(spyContext: Context): MeasurementManager {
+            val measurementManager = mock(MeasurementManager::class.java)
+            `when`(spyContext.getSystemService(MeasurementManager::class.java))
+                .thenReturn(measurementManager)
+            return measurementManager
+        }
+
+        private fun verifyDeletionRequest(request: android.adservices.measurement.DeletionRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.measurement.DeletionRequest.Builder()
+                .setDomainUris(listOf(uri1))
+                .setOriginUris(listOf(uri1))
+                .build()
+
+            assertThat(HashSet(request.domainUris) == HashSet(expectedRequest.domainUris))
+            assertThat(HashSet(request.originUris) == HashSet(expectedRequest.originUris))
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
new file mode 100644
index 0000000..36ce0d6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFuturesTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.topics
+
+import android.adservices.topics.Topic
+import android.adservices.topics.TopicsManager
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse
+import androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion.from
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class TopicsManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testTopicsOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testTopicsAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val topicsManager = mockTopicsManager(mContext)
+        setupTopicsResponse(topicsManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val request = GetTopicsRequest.Builder()
+            .setSdkName(mSdkName)
+            .setShouldRecordObservation(true)
+            .build()
+
+        val result: ListenableFuture<GetTopicsResponse> =
+            managerCompat!!.getTopicsAsync(request)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        verify(topicsManager).getTopics(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+    }
+
+    companion object {
+        private lateinit var mContext: Context
+        private val mSdkName: String = "sdk1"
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun mockTopicsManager(spyContext: Context): TopicsManager {
+            val topicsManager = mock(TopicsManager::class.java)
+            `when`(spyContext.getSystemService(TopicsManager::class.java))
+                .thenReturn(topicsManager)
+            return topicsManager
+        }
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun setupTopicsResponse(topicsManager: TopicsManager) {
+            // Set up the response that TopicsManager will return when the compat code calls it.
+            val topic1 = Topic(1, 1, 1)
+            val topic2 = Topic(2, 2, 2)
+            val topics = listOf(topic1, topic2)
+            val response = android.adservices.topics.GetTopicsResponse.Builder(topics).build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.topics.GetTopicsResponse, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(topicsManager).getTopics(
+                    any(),
+                    any(),
+                    any()
+                )
+        }
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun verifyRequest(topicsRequest: android.adservices.topics.GetTopicsRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.topics.GetTopicsRequest.Builder()
+                .setAdsSdkName(mSdkName)
+                .build()
+
+            Assert.assertEquals(expectedRequest.adsSdkName, topicsRequest.adsSdkName)
+        }
+
+        @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+        private fun verifyResponse(getTopicsResponse: GetTopicsResponse) {
+            Assert.assertEquals(2, getTopicsResponse.topics.size)
+            val topic1 = getTopicsResponse.topics[0]
+            val topic2 = getTopicsResponse.topics[1]
+            Assert.assertEquals(1, topic1.topicId)
+            Assert.assertEquals(1, topic1.modelVersion)
+            Assert.assertEquals(1, topic1.taxonomyVersion)
+            Assert.assertEquals(2, topic2.topicId)
+            Assert.assertEquals(2, topic2.modelVersion)
+            Assert.assertEquals(2, topic2.taxonomyVersion)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/res/xml/ad_services_config.xml b/privacysandbox/ads/ads-adservices-java/src/androidTest/res/xml/ad_services_config.xml
new file mode 100644
index 0000000..154098e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/res/xml/ad_services_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<ad-services-config>
+    <topics allowAllToAccess="true" />
+    <custom-audiences allowAllToAccess="true" />
+    <attribution allowAllToAccess="true" />
+</ad-services-config>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFutures.kt
new file mode 100644
index 0000000..07b112b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFutures.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adid
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.adid.AdId
+import androidx.privacysandbox.ads.adservices.adid.AdIdManager
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * AdId Manager provides APIs for app and ad-SDKs to access advertising ID. The advertising ID is a
+ * unique, per-device, user-resettable ID for advertising. It gives users better controls and
+ * provides developers with a simple, standard system to continue to monetize their apps via
+ * personalized ads (formerly known as interest-based ads). This class can be used by Java clients.
+ */
+abstract class AdIdManagerFutures internal constructor() {
+    /**
+     * Return the AdId.
+     *
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    abstract fun getAdIdAsync(): ListenableFuture<AdId>
+
+    private class Api33Ext4JavaImpl(private val mAdIdManager: AdIdManager) : AdIdManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+        override fun getAdIdAsync(): ListenableFuture<AdId> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mAdIdManager.getAdId()
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdIdManagerFutures].
+         *
+         *  @return AdIdManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): AdIdManagerFutures? {
+            return AdIdManager.obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFutures.kt
new file mode 100644
index 0000000..4726167
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFutures.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.adselection
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion.obtain
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.TransactionTooLargeException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+import androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This class provides APIs to select ads and report impressions.
+ * This class can be used by Java clients.
+ */
+abstract class AdSelectionManagerFutures internal constructor() {
+
+    /**
+     * Runs the ad selection process on device to select a remarketing ad for the caller
+     * application.
+     *
+     * @param adSelectionConfig the config The input {@code adSelectionConfig} is provided by the
+     * Ads SDK and the [AdSelectionConfig] object is transferred via a Binder call. For this
+     * reason, the total size of these objects is bound to the Android IPC limitations. Failures to
+     * transfer the [AdSelectionConfig] will throws an [TransactionTooLargeException].
+     *
+     * The output is passed by the receiver, which either returns an [AdSelectionOutcome]
+     * for a successful run, or an [Exception] includes the type of the exception thrown and
+     * the corresponding error message.
+     *
+     * If the [IllegalArgumentException] is thrown, it is caused by invalid input argument
+     * the API received to run the ad selection.
+     *
+     * If the [IllegalStateException] is thrown with error message "Failure of AdSelection
+     * services.", it is caused by an internal failure of the ad selection service.
+     *
+     * If the [TimeoutException] is thrown, it is caused when a timeout is encountered
+     * during bidding, scoring, or overall selection process to find winning Ad.
+     *
+     * If the [LimitExceededException] is thrown, it is caused when the calling package
+     * exceeds the allowed rate limits and is throttled.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun selectAdsAsync(
+        adSelectionConfig: AdSelectionConfig
+    ): ListenableFuture<AdSelectionOutcome>
+
+    /**
+     * Report the given impression. The [ReportImpressionRequest] is provided by the Ads SDK.
+     * The receiver either returns a {@code void} for a successful run, or an [Exception]
+     * indicates the error.
+     *
+     * @param reportImpressionRequest the request for reporting impression.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun reportImpressionAsync(
+        reportImpressionRequest: ReportImpressionRequest
+    ): ListenableFuture<Unit>
+
+    private class Api33Ext4JavaImpl(
+        private val mAdSelectionManager: AdSelectionManager?
+    ) : AdSelectionManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun selectAdsAsync(
+            adSelectionConfig: AdSelectionConfig
+        ): ListenableFuture<AdSelectionOutcome> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mAdSelectionManager!!.selectAds(adSelectionConfig)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun reportImpressionAsync(
+            reportImpressionRequest: ReportImpressionRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mAdSelectionManager!!.reportImpression(reportImpressionRequest)
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdSelectionManagerFutures].
+         *
+         *  @return AdSelectionManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): AdSelectionManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFutures.kt
new file mode 100644
index 0000000..f0812fd
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFutures.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.appsetid
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import android.content.Context
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetId
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * AppSetIdManager provides APIs for app and ad-SDKs to access appSetId for non-monetizing purpose.
+ * This class can be used by Java clients.
+ */
+abstract class AppSetIdManagerFutures internal constructor() {
+    /**
+     * Return the AppSetId.
+     *
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     */
+    abstract fun getAppSetIdAsync(): ListenableFuture<AppSetId>
+
+    private class Api33Ext4JavaImpl(
+        private val mAppSetIdManager: AppSetIdManager
+    ) : AppSetIdManagerFutures() {
+        @DoNotInline
+        override fun getAppSetIdAsync(): ListenableFuture<AppSetId> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mAppSetIdManager.getAppSetId()
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AppSetIdManagerFutures].
+         *
+         *  @return AppSetIdManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): AppSetIdManagerFutures? {
+            return AppSetIdManager.obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFutures.kt
new file mode 100644
index 0000000..cb43cf4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFutures.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.customaudience
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudience
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion.obtain
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This class provides APIs for app and ad-SDKs to join / leave custom audiences.
+ * This class can be used by Java clients.
+ */
+abstract class CustomAudienceManagerFutures internal constructor() {
+
+    /**
+     * Adds the user to the given [CustomAudience].
+     *
+     * An attempt to register the user for a custom audience with the same combination of {@code
+     * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
+     * information to be overwritten, including the list of ads data.
+     *
+     * Note that the ads list can be completely overwritten by the daily background fetch job.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with an [IllegalArgumentException] if
+     *
+     * <ol>
+     *   <li>the storage limit has been exceeded by the calling application and/or
+     *   <li>any URI parameters in the [CustomAudience] given are not authenticated with the
+     *       [CustomAudience] buyer.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call fails with an [IllegalStateException] if an internal service error is
+     * encountered.
+     *
+     * @param request The request to join custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun joinCustomAudienceAsync(
+        request: JoinCustomAudienceRequest
+    ): ListenableFuture<Unit>
+
+    /**
+     * Attempts to remove a user from a custom audience by deleting any existing
+     * [CustomAudience] data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
+     * name}.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call does not inform the caller whether the custom audience specified existed in
+     * on-device storage. In other words, it will fail silently when a buyer attempts to leave a
+     * custom audience that was not joined.
+     *
+     * @param request The request to leave custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract fun leaveCustomAudienceAsync(
+        request: LeaveCustomAudienceRequest
+    ): ListenableFuture<Unit>
+
+    private class Api33Ext4JavaImpl(
+        private val mCustomAudienceManager: CustomAudienceManager?
+    ) : CustomAudienceManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun joinCustomAudienceAsync(
+            request: JoinCustomAudienceRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mCustomAudienceManager!!.joinCustomAudience(request)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override fun leaveCustomAudienceAsync(
+            request: LeaveCustomAudienceRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Default).async {
+                mCustomAudienceManager!!.leaveCustomAudience(request)
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [CustomAudienceManagerFutures].
+         *
+         *  @return CustomAudienceManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): CustomAudienceManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/CoroutineAdapter.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/CoroutineAdapter.kt
new file mode 100644
index 0000000..b4f03c4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/CoroutineAdapter.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.internal
+
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@SuppressWarnings("AsyncSuffixFuture")
+@OptIn(ExperimentalCoroutinesApi::class)
+internal fun <T> Deferred<T>.asListenableFuture(
+    tag: Any? = "Deferred.asListenableFuture"
+): ListenableFuture<T> = CallbackToFutureAdapter.getFuture { completer ->
+
+    this.invokeOnCompletion {
+        if (it != null) {
+            if (it is CancellationException) {
+                completer.setCancelled()
+            } else {
+                completer.setException(it)
+            }
+        } else {
+            // Ignore exceptions - This should never throw in this situation.
+            completer.set(this.getCompleted())
+        }
+    }
+    tag
+}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/package-info.java
similarity index 79%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/package-info.java
index 7053e2d..a3c8ac4 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/internal/package-info.java
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.privacysandbox.ads.adservices.java.internal;
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.kt
new file mode 100644
index 0000000..dd39ab0
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/measurement/MeasurementManagerFutures.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.measurement
+
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import android.net.Uri
+import android.view.InputEvent
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest
+import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager
+import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This provides APIs for App and Ad-Sdks to access Privacy Sandbox Measurement APIs in a privacy
+ * preserving way. This class can be used by Java clients.
+ */
+abstract class MeasurementManagerFutures internal constructor() {
+    /**
+     * Delete previous registrations.
+     *
+     * @param deletionRequest The request for deleting data.
+     * @return ListenableFuture. If the deletion is successful, result is null.
+     */
+    @SuppressWarnings("MissingNullability")
+    abstract fun deleteRegistrationsAsync(
+        deletionRequest: DeletionRequest
+    ): ListenableFuture<Unit>
+
+    /**
+     * Register an attribution source (click or view).
+     *
+     * @param attributionSource the platform issues a request to this URI in order to fetch metadata
+     *     associated with the attribution source.
+     * @param inputEvent either an [InputEvent] object (for a click event) or null (for a view
+     *     event).
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerSourceAsync(
+        attributionSource: Uri,
+        inputEvent: InputEvent?
+    ): ListenableFuture<Unit>
+
+    /**
+     * Register a trigger (conversion).
+     *
+     * @param trigger the API issues a request to this URI to fetch metadata associated with the
+     *     trigger.
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerTriggerAsync(trigger: Uri): ListenableFuture<Unit>
+
+    /**
+     * Register an attribution source(click or view) from web context. This API will not process any
+     * redirects, all registration URLs should be supplied with the request. At least one of
+     * appDestination or webDestination parameters are required to be provided.
+     *
+     * @param request source registration request
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerWebSourceAsync(
+        request: WebSourceRegistrationRequest
+    ): ListenableFuture<Unit>
+
+    /**
+     * Register an attribution trigger(click or view) from web context. This API will not process
+     * any redirects, all registration URLs should be supplied with the request.
+     * OutcomeReceiver#onError}.
+     *
+     * @param request trigger registration request
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun registerWebTriggerAsync(
+        request: WebTriggerRegistrationRequest,
+    ): ListenableFuture<Unit>
+
+    /**
+     * Get Measurement API status.
+     *
+     * The call returns an integer value (see [MeasurementManager.MEASUREMENT_API_STATE_DISABLED]
+     * and [MeasurementManager.MEASUREMENT_API_STATE_ENABLED] for possible values).
+     */
+    @SuppressWarnings("MissingNullability")
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract fun getMeasurementApiStatusAsync(): ListenableFuture<Int>
+
+    private class Api33Ext4JavaImpl(
+        private val mMeasurementManager: MeasurementManager
+    ) : MeasurementManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun deleteRegistrationsAsync(
+            deletionRequest: DeletionRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.deleteRegistrations(deletionRequest)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerSourceAsync(
+            attributionSource: Uri,
+            inputEvent: InputEvent?
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerSource(attributionSource, inputEvent)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerTriggerAsync(trigger: Uri): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerTrigger(trigger)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerWebSourceAsync(
+            request: WebSourceRegistrationRequest
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerWebSource(request)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun registerWebTriggerAsync(
+            request: WebTriggerRegistrationRequest,
+        ): ListenableFuture<Unit> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.registerWebTrigger(request)
+            }.asListenableFuture()
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION)
+        override fun getMeasurementApiStatusAsync(): ListenableFuture<Int> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mMeasurementManager.getMeasurementApiStatus()
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [MeasurementManagerFutures].
+         *
+         *  @return MeasurementManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): MeasurementManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFutures.kt b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFutures.kt
new file mode 100644
index 0000000..5f80cd7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/main/java/androidx/privacysandbox/ads/adservices/java/topics/TopicsManagerFutures.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.privacysandbox.ads.adservices.java.topics
+
+import androidx.privacysandbox.ads.adservices.java.internal.asListenableFuture
+import androidx.privacysandbox.ads.adservices.topics.TopicsManager
+import androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest
+import androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse
+import android.adservices.common.AdServicesPermissions
+import android.content.Context
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresPermission
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * This provides APIs for App and Ad-Sdks to get the user interest topics in a privacy
+ * preserving way. This class can be used by Java clients.
+ */
+abstract class TopicsManagerFutures internal constructor() {
+    /**
+     * Returns the topics.
+     *
+     * @param request The GetTopicsRequest for obtaining Topics.
+     * @return ListenableFuture to get the Topics response.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+    abstract fun getTopicsAsync(request: GetTopicsRequest): ListenableFuture<GetTopicsResponse>
+
+    private class Api33Ext4JavaImpl(
+        private val mTopicsManager: TopicsManager
+    ) : TopicsManagerFutures() {
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+        override fun getTopicsAsync(
+            request: GetTopicsRequest
+        ): ListenableFuture<GetTopicsResponse> {
+            return CoroutineScope(Dispatchers.Main).async {
+                mTopicsManager.getTopics(request)
+            }.asListenableFuture()
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [TopicsManagerFutures].
+         *
+         *  @return TopicsManagerFutures object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        fun from(context: Context): TopicsManagerFutures? {
+            return obtain(context)?.let { Api33Ext4JavaImpl(it) }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/api/current.txt b/privacysandbox/ads/ads-adservices/api/current.txt
new file mode 100644
index 0000000..01aa5c6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/current.txt
@@ -0,0 +1,345 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.adid {
+
+  public final class AdId {
+    method public String getAdId();
+    method public boolean isLimitAdTrackingEnabled();
+    property public final String adId;
+    property public final boolean isLimitAdTrackingEnabled;
+  }
+
+  public abstract class AdIdManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract suspend Object? getAdId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adid.AdId>);
+    method public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager.Companion Companion;
+  }
+
+  public static final class AdIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.adselection {
+
+  public final class AdSelectionConfig {
+    ctor public AdSelectionConfig(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller, android.net.Uri decisionLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals, java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals, android.net.Uri trustedScoringSignalsUri);
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getAdSelectionSignals();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> getCustomAudienceBuyers();
+    method public android.net.Uri getDecisionLogicUri();
+    method public java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> getPerBuyerSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getSeller();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getSellerSignals();
+    method public android.net.Uri getTrustedScoringSignalsUri();
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers;
+    property public final android.net.Uri decisionLogicUri;
+    property public final java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals;
+    property public final android.net.Uri trustedScoringSignalsUri;
+  }
+
+  public abstract class AdSelectionManager {
+    method public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? reportImpression(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? selectAds(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome>);
+    field public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion Companion;
+  }
+
+  public static final class AdSelectionManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+  }
+
+  public final class AdSelectionOutcome {
+    ctor public AdSelectionOutcome(long adSelectionId, android.net.Uri renderUri);
+    method public long getAdSelectionId();
+    method public android.net.Uri getRenderUri();
+    property public final long adSelectionId;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class ReportImpressionRequest {
+    ctor public ReportImpressionRequest(long adSelectionId, androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig getAdSelectionConfig();
+    method public long getAdSelectionId();
+    property public final androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig;
+    property public final long adSelectionId;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.appsetid {
+
+  public final class AppSetId {
+    ctor public AppSetId(String id, int scope);
+    method public String getId();
+    method public int getScope();
+    property public final String id;
+    property public final int scope;
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetId.Companion Companion;
+    field public static final int SCOPE_APP = 1; // 0x1
+    field public static final int SCOPE_DEVELOPER = 2; // 0x2
+  }
+
+  public static final class AppSetId.Companion {
+  }
+
+  public abstract class AppSetIdManager {
+    method public abstract suspend Object? getAppSetId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.appsetid.AppSetId>);
+    method public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager.Companion Companion;
+  }
+
+  public static final class AppSetIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.common {
+
+  public final class AdData {
+    ctor public AdData(android.net.Uri renderUri, String metadata);
+    method public String getMetadata();
+    method public android.net.Uri getRenderUri();
+    property public final String metadata;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class AdSelectionSignals {
+    ctor public AdSelectionSignals(String signals);
+    method public String getSignals();
+    property public final String signals;
+  }
+
+  public final class AdTechIdentifier {
+    ctor public AdTechIdentifier(String identifier);
+    method public String getIdentifier();
+    property public final String identifier;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.customaudience {
+
+  public final class CustomAudience {
+    ctor public CustomAudience(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads, optional java.time.Instant? activationTime, optional java.time.Instant? expirationTime, optional androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals, optional androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals);
+    method public java.time.Instant? getActivationTime();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> getAds();
+    method public android.net.Uri getBiddingLogicUri();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public android.net.Uri getDailyUpdateUri();
+    method public java.time.Instant? getExpirationTime();
+    method public String getName();
+    method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? getTrustedBiddingSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? getUserBiddingSignals();
+    property public final java.time.Instant? activationTime;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads;
+    property public final android.net.Uri biddingLogicUri;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final android.net.Uri dailyUpdateUri;
+    property public final java.time.Instant? expirationTime;
+    property public final String name;
+    property public final androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals;
+  }
+
+  public static final class CustomAudience.Builder {
+    ctor public CustomAudience.Builder(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience build();
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setActivationTime(java.time.Instant activationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setAds(java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBiddingLogicUri(android.net.Uri biddingLogicUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBuyer(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setDailyUpdateUri(android.net.Uri dailyUpdateUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setExpirationTime(java.time.Instant expirationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setName(String name);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setTrustedBiddingData(androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData trustedBiddingSignals);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setUserBiddingSignals(androidx.privacysandbox.ads.adservices.common.AdSelectionSignals userBiddingSignals);
+  }
+
+  public abstract class CustomAudienceManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? joinCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? leaveCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion Companion;
+  }
+
+  public static final class CustomAudienceManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+  }
+
+  public final class JoinCustomAudienceRequest {
+    ctor public JoinCustomAudienceRequest(androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience getCustomAudience();
+    property public final androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience;
+  }
+
+  public final class LeaveCustomAudienceRequest {
+    ctor public LeaveCustomAudienceRequest(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name);
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public String getName();
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final String name;
+  }
+
+  public final class TrustedBiddingData {
+    ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+    method public java.util.List<java.lang.String> getTrustedBiddingKeys();
+    method public android.net.Uri getTrustedBiddingUri();
+    property public final java.util.List<java.lang.String> trustedBiddingKeys;
+    property public final android.net.Uri trustedBiddingUri;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.measurement {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class DeletionRequest {
+    ctor public DeletionRequest(int deletionMode, int matchBehavior, optional java.time.Instant start, optional java.time.Instant end, optional java.util.List<? extends android.net.Uri> domainUris, optional java.util.List<? extends android.net.Uri> originUris);
+    method public int getDeletionMode();
+    method public java.util.List<android.net.Uri> getDomainUris();
+    method public java.time.Instant getEnd();
+    method public int getMatchBehavior();
+    method public java.util.List<android.net.Uri> getOriginUris();
+    method public java.time.Instant getStart();
+    property public final int deletionMode;
+    property public final java.util.List<android.net.Uri> domainUris;
+    property public final java.time.Instant end;
+    property public final int matchBehavior;
+    property public final java.util.List<android.net.Uri> originUris;
+    property public final java.time.Instant start;
+    field public static final androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Companion Companion;
+    field public static final int DELETION_MODE_ALL = 0; // 0x0
+    field public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1; // 0x1
+    field public static final int MATCH_BEHAVIOR_DELETE = 0; // 0x0
+    field public static final int MATCH_BEHAVIOR_PRESERVE = 1; // 0x1
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class DeletionRequest.Builder {
+    ctor public DeletionRequest.Builder(int deletionMode, int matchBehavior);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setDomainUris(java.util.List<? extends android.net.Uri> domainUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setEnd(java.time.Instant end);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setOriginUris(java.util.List<? extends android.net.Uri> originUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setStart(java.time.Instant start);
+  }
+
+  public static final class DeletionRequest.Companion {
+  }
+
+  public abstract class MeasurementManager {
+    ctor public MeasurementManager();
+    method public abstract suspend Object? deleteRegistrations(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? getMeasurementApiStatus(kotlin.coroutines.Continuation<? super java.lang.Integer>);
+    method public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerSource(android.net.Uri attributionSource, android.view.InputEvent? inputEvent, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerTrigger(android.net.Uri trigger, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebSource(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebTrigger(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    field public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion Companion;
+    field public static final int MEASUREMENT_API_STATE_DISABLED = 0; // 0x0
+    field public static final int MEASUREMENT_API_STATE_ENABLED = 1; // 0x1
+  }
+
+  public static final class MeasurementManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceParams {
+    ctor public WebSourceParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceRegistrationRequest {
+    ctor public WebSourceRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri, optional android.view.InputEvent? inputEvent, optional android.net.Uri? appDestination, optional android.net.Uri? webDestination, optional android.net.Uri? verifiedDestination);
+    method public android.net.Uri? getAppDestination();
+    method public android.view.InputEvent? getInputEvent();
+    method public android.net.Uri getTopOriginUri();
+    method public android.net.Uri? getVerifiedDestination();
+    method public android.net.Uri? getWebDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> getWebSourceParams();
+    property public final android.net.Uri? appDestination;
+    property public final android.view.InputEvent? inputEvent;
+    property public final android.net.Uri topOriginUri;
+    property public final android.net.Uri? verifiedDestination;
+    property public final android.net.Uri? webDestination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams;
+  }
+
+  public static final class WebSourceRegistrationRequest.Builder {
+    ctor public WebSourceRegistrationRequest.Builder(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setAppDestination(android.net.Uri? appDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setInputEvent(android.view.InputEvent inputEvent);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setVerifiedDestination(android.net.Uri? verifiedDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setWebDestination(android.net.Uri? webDestination);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerParams {
+    ctor public WebTriggerParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerRegistrationRequest {
+    ctor public WebTriggerRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams, android.net.Uri destination);
+    method public android.net.Uri getDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> getWebTriggerParams();
+    property public final android.net.Uri destination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.topics {
+
+  public final class GetTopicsRequest {
+    ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
+    method public String getSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String sdkName;
+    property public final boolean shouldRecordObservation;
+  }
+
+  public static final class GetTopicsRequest.Builder {
+    ctor public GetTopicsRequest.Builder();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
+  }
+
+  public final class GetTopicsResponse {
+    ctor public GetTopicsResponse(java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics);
+    method public java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> getTopics();
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics;
+  }
+
+  public final class Topic {
+    ctor public Topic(long taxonomyVersion, long modelVersion, int topicId);
+    method public long getModelVersion();
+    method public long getTaxonomyVersion();
+    method public int getTopicId();
+    property public final long modelVersion;
+    property public final long taxonomyVersion;
+    property public final int topicId;
+  }
+
+  public abstract class TopicsManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract suspend Object? getTopics(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse>);
+    method public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion Companion;
+  }
+
+  public static final class TopicsManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..01aa5c6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/public_plus_experimental_current.txt
@@ -0,0 +1,345 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.adid {
+
+  public final class AdId {
+    method public String getAdId();
+    method public boolean isLimitAdTrackingEnabled();
+    property public final String adId;
+    property public final boolean isLimitAdTrackingEnabled;
+  }
+
+  public abstract class AdIdManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract suspend Object? getAdId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adid.AdId>);
+    method public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager.Companion Companion;
+  }
+
+  public static final class AdIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.adselection {
+
+  public final class AdSelectionConfig {
+    ctor public AdSelectionConfig(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller, android.net.Uri decisionLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals, java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals, android.net.Uri trustedScoringSignalsUri);
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getAdSelectionSignals();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> getCustomAudienceBuyers();
+    method public android.net.Uri getDecisionLogicUri();
+    method public java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> getPerBuyerSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getSeller();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getSellerSignals();
+    method public android.net.Uri getTrustedScoringSignalsUri();
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers;
+    property public final android.net.Uri decisionLogicUri;
+    property public final java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals;
+    property public final android.net.Uri trustedScoringSignalsUri;
+  }
+
+  public abstract class AdSelectionManager {
+    method public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? reportImpression(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? selectAds(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome>);
+    field public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion Companion;
+  }
+
+  public static final class AdSelectionManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+  }
+
+  public final class AdSelectionOutcome {
+    ctor public AdSelectionOutcome(long adSelectionId, android.net.Uri renderUri);
+    method public long getAdSelectionId();
+    method public android.net.Uri getRenderUri();
+    property public final long adSelectionId;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class ReportImpressionRequest {
+    ctor public ReportImpressionRequest(long adSelectionId, androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig getAdSelectionConfig();
+    method public long getAdSelectionId();
+    property public final androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig;
+    property public final long adSelectionId;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.appsetid {
+
+  public final class AppSetId {
+    ctor public AppSetId(String id, int scope);
+    method public String getId();
+    method public int getScope();
+    property public final String id;
+    property public final int scope;
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetId.Companion Companion;
+    field public static final int SCOPE_APP = 1; // 0x1
+    field public static final int SCOPE_DEVELOPER = 2; // 0x2
+  }
+
+  public static final class AppSetId.Companion {
+  }
+
+  public abstract class AppSetIdManager {
+    method public abstract suspend Object? getAppSetId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.appsetid.AppSetId>);
+    method public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager.Companion Companion;
+  }
+
+  public static final class AppSetIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.common {
+
+  public final class AdData {
+    ctor public AdData(android.net.Uri renderUri, String metadata);
+    method public String getMetadata();
+    method public android.net.Uri getRenderUri();
+    property public final String metadata;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class AdSelectionSignals {
+    ctor public AdSelectionSignals(String signals);
+    method public String getSignals();
+    property public final String signals;
+  }
+
+  public final class AdTechIdentifier {
+    ctor public AdTechIdentifier(String identifier);
+    method public String getIdentifier();
+    property public final String identifier;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.customaudience {
+
+  public final class CustomAudience {
+    ctor public CustomAudience(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads, optional java.time.Instant? activationTime, optional java.time.Instant? expirationTime, optional androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals, optional androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals);
+    method public java.time.Instant? getActivationTime();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> getAds();
+    method public android.net.Uri getBiddingLogicUri();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public android.net.Uri getDailyUpdateUri();
+    method public java.time.Instant? getExpirationTime();
+    method public String getName();
+    method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? getTrustedBiddingSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? getUserBiddingSignals();
+    property public final java.time.Instant? activationTime;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads;
+    property public final android.net.Uri biddingLogicUri;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final android.net.Uri dailyUpdateUri;
+    property public final java.time.Instant? expirationTime;
+    property public final String name;
+    property public final androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals;
+  }
+
+  public static final class CustomAudience.Builder {
+    ctor public CustomAudience.Builder(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience build();
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setActivationTime(java.time.Instant activationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setAds(java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBiddingLogicUri(android.net.Uri biddingLogicUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBuyer(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setDailyUpdateUri(android.net.Uri dailyUpdateUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setExpirationTime(java.time.Instant expirationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setName(String name);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setTrustedBiddingData(androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData trustedBiddingSignals);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setUserBiddingSignals(androidx.privacysandbox.ads.adservices.common.AdSelectionSignals userBiddingSignals);
+  }
+
+  public abstract class CustomAudienceManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? joinCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? leaveCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion Companion;
+  }
+
+  public static final class CustomAudienceManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+  }
+
+  public final class JoinCustomAudienceRequest {
+    ctor public JoinCustomAudienceRequest(androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience getCustomAudience();
+    property public final androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience;
+  }
+
+  public final class LeaveCustomAudienceRequest {
+    ctor public LeaveCustomAudienceRequest(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name);
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public String getName();
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final String name;
+  }
+
+  public final class TrustedBiddingData {
+    ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+    method public java.util.List<java.lang.String> getTrustedBiddingKeys();
+    method public android.net.Uri getTrustedBiddingUri();
+    property public final java.util.List<java.lang.String> trustedBiddingKeys;
+    property public final android.net.Uri trustedBiddingUri;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.measurement {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class DeletionRequest {
+    ctor public DeletionRequest(int deletionMode, int matchBehavior, optional java.time.Instant start, optional java.time.Instant end, optional java.util.List<? extends android.net.Uri> domainUris, optional java.util.List<? extends android.net.Uri> originUris);
+    method public int getDeletionMode();
+    method public java.util.List<android.net.Uri> getDomainUris();
+    method public java.time.Instant getEnd();
+    method public int getMatchBehavior();
+    method public java.util.List<android.net.Uri> getOriginUris();
+    method public java.time.Instant getStart();
+    property public final int deletionMode;
+    property public final java.util.List<android.net.Uri> domainUris;
+    property public final java.time.Instant end;
+    property public final int matchBehavior;
+    property public final java.util.List<android.net.Uri> originUris;
+    property public final java.time.Instant start;
+    field public static final androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Companion Companion;
+    field public static final int DELETION_MODE_ALL = 0; // 0x0
+    field public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1; // 0x1
+    field public static final int MATCH_BEHAVIOR_DELETE = 0; // 0x0
+    field public static final int MATCH_BEHAVIOR_PRESERVE = 1; // 0x1
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class DeletionRequest.Builder {
+    ctor public DeletionRequest.Builder(int deletionMode, int matchBehavior);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setDomainUris(java.util.List<? extends android.net.Uri> domainUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setEnd(java.time.Instant end);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setOriginUris(java.util.List<? extends android.net.Uri> originUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setStart(java.time.Instant start);
+  }
+
+  public static final class DeletionRequest.Companion {
+  }
+
+  public abstract class MeasurementManager {
+    ctor public MeasurementManager();
+    method public abstract suspend Object? deleteRegistrations(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? getMeasurementApiStatus(kotlin.coroutines.Continuation<? super java.lang.Integer>);
+    method public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerSource(android.net.Uri attributionSource, android.view.InputEvent? inputEvent, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerTrigger(android.net.Uri trigger, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebSource(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebTrigger(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    field public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion Companion;
+    field public static final int MEASUREMENT_API_STATE_DISABLED = 0; // 0x0
+    field public static final int MEASUREMENT_API_STATE_ENABLED = 1; // 0x1
+  }
+
+  public static final class MeasurementManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceParams {
+    ctor public WebSourceParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceRegistrationRequest {
+    ctor public WebSourceRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri, optional android.view.InputEvent? inputEvent, optional android.net.Uri? appDestination, optional android.net.Uri? webDestination, optional android.net.Uri? verifiedDestination);
+    method public android.net.Uri? getAppDestination();
+    method public android.view.InputEvent? getInputEvent();
+    method public android.net.Uri getTopOriginUri();
+    method public android.net.Uri? getVerifiedDestination();
+    method public android.net.Uri? getWebDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> getWebSourceParams();
+    property public final android.net.Uri? appDestination;
+    property public final android.view.InputEvent? inputEvent;
+    property public final android.net.Uri topOriginUri;
+    property public final android.net.Uri? verifiedDestination;
+    property public final android.net.Uri? webDestination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams;
+  }
+
+  public static final class WebSourceRegistrationRequest.Builder {
+    ctor public WebSourceRegistrationRequest.Builder(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setAppDestination(android.net.Uri? appDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setInputEvent(android.view.InputEvent inputEvent);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setVerifiedDestination(android.net.Uri? verifiedDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setWebDestination(android.net.Uri? webDestination);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerParams {
+    ctor public WebTriggerParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerRegistrationRequest {
+    ctor public WebTriggerRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams, android.net.Uri destination);
+    method public android.net.Uri getDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> getWebTriggerParams();
+    property public final android.net.Uri destination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.topics {
+
+  public final class GetTopicsRequest {
+    ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
+    method public String getSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String sdkName;
+    property public final boolean shouldRecordObservation;
+  }
+
+  public static final class GetTopicsRequest.Builder {
+    ctor public GetTopicsRequest.Builder();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
+  }
+
+  public final class GetTopicsResponse {
+    ctor public GetTopicsResponse(java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics);
+    method public java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> getTopics();
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics;
+  }
+
+  public final class Topic {
+    ctor public Topic(long taxonomyVersion, long modelVersion, int topicId);
+    method public long getModelVersion();
+    method public long getTaxonomyVersion();
+    method public int getTopicId();
+    property public final long modelVersion;
+    property public final long taxonomyVersion;
+    property public final int topicId;
+  }
+
+  public abstract class TopicsManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract suspend Object? getTopics(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse>);
+    method public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion Companion;
+  }
+
+  public static final class TopicsManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+  }
+
+}
+
diff --git a/webkit/webkit/api/res-1.6.0-beta02.txt b/privacysandbox/ads/ads-adservices/api/res-current.txt
similarity index 100%
rename from webkit/webkit/api/res-1.6.0-beta02.txt
rename to privacysandbox/ads/ads-adservices/api/res-current.txt
diff --git a/privacysandbox/ads/ads-adservices/api/restricted_current.txt b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
new file mode 100644
index 0000000..01aa5c6
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
@@ -0,0 +1,345 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.adid {
+
+  public final class AdId {
+    method public String getAdId();
+    method public boolean isLimitAdTrackingEnabled();
+    property public final String adId;
+    property public final boolean isLimitAdTrackingEnabled;
+  }
+
+  public abstract class AdIdManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract suspend Object? getAdId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adid.AdId>);
+    method public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.adid.AdIdManager.Companion Companion;
+  }
+
+  public static final class AdIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adid.AdIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.adselection {
+
+  public final class AdSelectionConfig {
+    ctor public AdSelectionConfig(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller, android.net.Uri decisionLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals, androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals, java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals, android.net.Uri trustedScoringSignalsUri);
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getAdSelectionSignals();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> getCustomAudienceBuyers();
+    method public android.net.Uri getDecisionLogicUri();
+    method public java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> getPerBuyerSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getSeller();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals getSellerSignals();
+    method public android.net.Uri getTrustedScoringSignalsUri();
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals adSelectionSignals;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier> customAudienceBuyers;
+    property public final android.net.Uri decisionLogicUri;
+    property public final java.util.Map<androidx.privacysandbox.ads.adservices.common.AdTechIdentifier,androidx.privacysandbox.ads.adservices.common.AdSelectionSignals> perBuyerSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier seller;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals sellerSignals;
+    property public final android.net.Uri trustedScoringSignalsUri;
+  }
+
+  public abstract class AdSelectionManager {
+    method public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? reportImpression(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? selectAds(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome>);
+    field public static final androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion Companion;
+  }
+
+  public static final class AdSelectionManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager? obtain(android.content.Context context);
+  }
+
+  public final class AdSelectionOutcome {
+    ctor public AdSelectionOutcome(long adSelectionId, android.net.Uri renderUri);
+    method public long getAdSelectionId();
+    method public android.net.Uri getRenderUri();
+    property public final long adSelectionId;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class ReportImpressionRequest {
+    ctor public ReportImpressionRequest(long adSelectionId, androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    method public androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig getAdSelectionConfig();
+    method public long getAdSelectionId();
+    property public final androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig;
+    property public final long adSelectionId;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.appsetid {
+
+  public final class AppSetId {
+    ctor public AppSetId(String id, int scope);
+    method public String getId();
+    method public int getScope();
+    property public final String id;
+    property public final int scope;
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetId.Companion Companion;
+    field public static final int SCOPE_APP = 1; // 0x1
+    field public static final int SCOPE_DEVELOPER = 2; // 0x2
+  }
+
+  public static final class AppSetId.Companion {
+  }
+
+  public abstract class AppSetIdManager {
+    method public abstract suspend Object? getAppSetId(kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.appsetid.AppSetId>);
+    method public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager.Companion Companion;
+  }
+
+  public static final class AppSetIdManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.appsetid.AppSetIdManager? obtain(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.common {
+
+  public final class AdData {
+    ctor public AdData(android.net.Uri renderUri, String metadata);
+    method public String getMetadata();
+    method public android.net.Uri getRenderUri();
+    property public final String metadata;
+    property public final android.net.Uri renderUri;
+  }
+
+  public final class AdSelectionSignals {
+    ctor public AdSelectionSignals(String signals);
+    method public String getSignals();
+    property public final String signals;
+  }
+
+  public final class AdTechIdentifier {
+    ctor public AdTechIdentifier(String identifier);
+    method public String getIdentifier();
+    property public final String identifier;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.customaudience {
+
+  public final class CustomAudience {
+    ctor public CustomAudience(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads, optional java.time.Instant? activationTime, optional java.time.Instant? expirationTime, optional androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals, optional androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals);
+    method public java.time.Instant? getActivationTime();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> getAds();
+    method public android.net.Uri getBiddingLogicUri();
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public android.net.Uri getDailyUpdateUri();
+    method public java.time.Instant? getExpirationTime();
+    method public String getName();
+    method public androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? getTrustedBiddingSignals();
+    method public androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? getUserBiddingSignals();
+    property public final java.time.Instant? activationTime;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads;
+    property public final android.net.Uri biddingLogicUri;
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final android.net.Uri dailyUpdateUri;
+    property public final java.time.Instant? expirationTime;
+    property public final String name;
+    property public final androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData? trustedBiddingSignals;
+    property public final androidx.privacysandbox.ads.adservices.common.AdSelectionSignals? userBiddingSignals;
+  }
+
+  public static final class CustomAudience.Builder {
+    ctor public CustomAudience.Builder(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name, android.net.Uri dailyUpdateUri, android.net.Uri biddingLogicUri, java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience build();
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setActivationTime(java.time.Instant activationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setAds(java.util.List<androidx.privacysandbox.ads.adservices.common.AdData> ads);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBiddingLogicUri(android.net.Uri biddingLogicUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setBuyer(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setDailyUpdateUri(android.net.Uri dailyUpdateUri);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setExpirationTime(java.time.Instant expirationTime);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setName(String name);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setTrustedBiddingData(androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData trustedBiddingSignals);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience.Builder setUserBiddingSignals(androidx.privacysandbox.ads.adservices.common.AdSelectionSignals userBiddingSignals);
+  }
+
+  public abstract class CustomAudienceManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? joinCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract suspend Object? leaveCustomAudience(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion Companion;
+  }
+
+  public static final class CustomAudienceManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager? obtain(android.content.Context context);
+  }
+
+  public final class JoinCustomAudienceRequest {
+    ctor public JoinCustomAudienceRequest(androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience);
+    method public androidx.privacysandbox.ads.adservices.customaudience.CustomAudience getCustomAudience();
+    property public final androidx.privacysandbox.ads.adservices.customaudience.CustomAudience customAudience;
+  }
+
+  public final class LeaveCustomAudienceRequest {
+    ctor public LeaveCustomAudienceRequest(androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer, String name);
+    method public androidx.privacysandbox.ads.adservices.common.AdTechIdentifier getBuyer();
+    method public String getName();
+    property public final androidx.privacysandbox.ads.adservices.common.AdTechIdentifier buyer;
+    property public final String name;
+  }
+
+  public final class TrustedBiddingData {
+    ctor public TrustedBiddingData(android.net.Uri trustedBiddingUri, java.util.List<java.lang.String> trustedBiddingKeys);
+    method public java.util.List<java.lang.String> getTrustedBiddingKeys();
+    method public android.net.Uri getTrustedBiddingUri();
+    property public final java.util.List<java.lang.String> trustedBiddingKeys;
+    property public final android.net.Uri trustedBiddingUri;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.measurement {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class DeletionRequest {
+    ctor public DeletionRequest(int deletionMode, int matchBehavior, optional java.time.Instant start, optional java.time.Instant end, optional java.util.List<? extends android.net.Uri> domainUris, optional java.util.List<? extends android.net.Uri> originUris);
+    method public int getDeletionMode();
+    method public java.util.List<android.net.Uri> getDomainUris();
+    method public java.time.Instant getEnd();
+    method public int getMatchBehavior();
+    method public java.util.List<android.net.Uri> getOriginUris();
+    method public java.time.Instant getStart();
+    property public final int deletionMode;
+    property public final java.util.List<android.net.Uri> domainUris;
+    property public final java.time.Instant end;
+    property public final int matchBehavior;
+    property public final java.util.List<android.net.Uri> originUris;
+    property public final java.time.Instant start;
+    field public static final androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Companion Companion;
+    field public static final int DELETION_MODE_ALL = 0; // 0x0
+    field public static final int DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1; // 0x1
+    field public static final int MATCH_BEHAVIOR_DELETE = 0; // 0x0
+    field public static final int MATCH_BEHAVIOR_PRESERVE = 1; // 0x1
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class DeletionRequest.Builder {
+    ctor public DeletionRequest.Builder(int deletionMode, int matchBehavior);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setDomainUris(java.util.List<? extends android.net.Uri> domainUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setEnd(java.time.Instant end);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setOriginUris(java.util.List<? extends android.net.Uri> originUris);
+    method public androidx.privacysandbox.ads.adservices.measurement.DeletionRequest.Builder setStart(java.time.Instant start);
+  }
+
+  public static final class DeletionRequest.Companion {
+  }
+
+  public abstract class MeasurementManager {
+    ctor public MeasurementManager();
+    method public abstract suspend Object? deleteRegistrations(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? getMeasurementApiStatus(kotlin.coroutines.Continuation<? super java.lang.Integer>);
+    method public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerSource(android.net.Uri attributionSource, android.view.InputEvent? inputEvent, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerTrigger(android.net.Uri trigger, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebSource(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract suspend Object? registerWebTrigger(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    field public static final androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion Companion;
+    field public static final int MEASUREMENT_API_STATE_DISABLED = 0; // 0x0
+    field public static final int MEASUREMENT_API_STATE_ENABLED = 1; // 0x1
+  }
+
+  public static final class MeasurementManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.measurement.MeasurementManager? obtain(android.content.Context context);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceParams {
+    ctor public WebSourceParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebSourceRegistrationRequest {
+    ctor public WebSourceRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri, optional android.view.InputEvent? inputEvent, optional android.net.Uri? appDestination, optional android.net.Uri? webDestination, optional android.net.Uri? verifiedDestination);
+    method public android.net.Uri? getAppDestination();
+    method public android.view.InputEvent? getInputEvent();
+    method public android.net.Uri getTopOriginUri();
+    method public android.net.Uri? getVerifiedDestination();
+    method public android.net.Uri? getWebDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> getWebSourceParams();
+    property public final android.net.Uri? appDestination;
+    property public final android.view.InputEvent? inputEvent;
+    property public final android.net.Uri topOriginUri;
+    property public final android.net.Uri? verifiedDestination;
+    property public final android.net.Uri? webDestination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams;
+  }
+
+  public static final class WebSourceRegistrationRequest.Builder {
+    ctor public WebSourceRegistrationRequest.Builder(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebSourceParams> webSourceParams, android.net.Uri topOriginUri);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest build();
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setAppDestination(android.net.Uri? appDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setInputEvent(android.view.InputEvent inputEvent);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setVerifiedDestination(android.net.Uri? verifiedDestination);
+    method public androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest.Builder setWebDestination(android.net.Uri? webDestination);
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerParams {
+    ctor public WebTriggerParams(android.net.Uri registrationUri, boolean debugKeyAllowed);
+    method public boolean getDebugKeyAllowed();
+    method public android.net.Uri getRegistrationUri();
+    property public final boolean debugKeyAllowed;
+    property public final android.net.Uri registrationUri;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WebTriggerRegistrationRequest {
+    ctor public WebTriggerRegistrationRequest(java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams, android.net.Uri destination);
+    method public android.net.Uri getDestination();
+    method public java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> getWebTriggerParams();
+    property public final android.net.Uri destination;
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams> webTriggerParams;
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.topics {
+
+  public final class GetTopicsRequest {
+    ctor public GetTopicsRequest(optional String sdkName, optional boolean shouldRecordObservation);
+    method public String getSdkName();
+    method public boolean getShouldRecordObservation();
+    property public final String sdkName;
+    property public final boolean shouldRecordObservation;
+  }
+
+  public static final class GetTopicsRequest.Builder {
+    ctor public GetTopicsRequest.Builder();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest build();
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setSdkName(String sdkName);
+    method public androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest.Builder setShouldRecordObservation(boolean shouldRecordObservation);
+  }
+
+  public final class GetTopicsResponse {
+    ctor public GetTopicsResponse(java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics);
+    method public java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> getTopics();
+    property public final java.util.List<androidx.privacysandbox.ads.adservices.topics.Topic> topics;
+  }
+
+  public final class Topic {
+    ctor public Topic(long taxonomyVersion, long modelVersion, int topicId);
+    method public long getModelVersion();
+    method public long getTaxonomyVersion();
+    method public int getTopicId();
+    property public final long modelVersion;
+    property public final long taxonomyVersion;
+    property public final int topicId;
+  }
+
+  public abstract class TopicsManager {
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract suspend Object? getTopics(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request, kotlin.coroutines.Continuation<? super androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse>);
+    method public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+    field public static final androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion Companion;
+  }
+
+  public static final class TopicsManager.Companion {
+    method public androidx.privacysandbox.ads.adservices.topics.TopicsManager? obtain(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices/build.gradle b/privacysandbox/ads/ads-adservices/build.gradle
new file mode 100644
index 0000000..2679d8e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/build.gradle
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.Publish
+import androidx.build.RunApiTasks
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core-ktx:1.8.0")
+    api(projectOrArtifact(":annotation:annotation"))
+
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(project(":internal-testutils-truth"))
+
+    androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+}
+
+android {
+    compileSdk = 33
+    compileSdkExtension = 4
+    namespace "androidx.privacysandbox.ads.adservices"
+}
+
+androidx {
+    name = "Androidx library for Privacy Preserving APIs."
+    type = LibraryType.PUBLISHED_LIBRARY
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    inceptionYear = "2022"
+    description = "This library enables integration with Privacy Preserving APIs, which are part of Privacy Sandbox on Android."
+}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/AndroidManifest.xml b/privacysandbox/ads/ads-adservices/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3f2e804
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
new file mode 100644
index 0000000..8d71955
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdManagerTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+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 kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdIdManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AdIdManager.obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAdIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adIdManager = mockAdIdManager(mContext)
+        setupResponse(adIdManager)
+        val managerCompat = AdIdManager.obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            managerCompat!!.getAdId()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(adIdManager).getAdId(any(), any())
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+            val adIdManager = mock(android.adservices.adid.AdIdManager::class.java)
+            `when`(spyContext.getSystemService(android.adservices.adid.AdIdManager::class.java))
+                .thenReturn(adIdManager)
+            return adIdManager
+        }
+
+        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adIdManager).getAdId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(adId: AdId) {
+            Assert.assertEquals("1234", adId.adId)
+            Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdTest.kt
new file mode 100644
index 0000000..516d3fe
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adid/AdIdTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdIdTest {
+    @Test
+    fun testToString() {
+        val result = "AdId: adId=1234, isLimitAdTrackingEnabled=false"
+        val adId = AdId("1234", false)
+        Truth.assertThat(adId.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adId1 = AdId("1234", false)
+        val adId2 = AdId("1234", false)
+        Truth.assertThat(adId1 == adId2).isTrue()
+
+        val adId3 = AdId("1234", true)
+        Truth.assertThat(adId1 == adId3).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfigTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfigTest.kt
new file mode 100644
index 0000000..61d15e4
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfigTest.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdSelectionConfigTest {
+    private val seller: AdTechIdentifier = AdTechIdentifier("1234")
+    private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+    private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+    private val adSelectionSignals: AdSelectionSignals = AdSelectionSignals("adSelSignals")
+    private val sellerSignals: AdSelectionSignals = AdSelectionSignals("sellerSignals")
+    private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+        mutableMapOf(Pair(seller, sellerSignals))
+    private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+    @Test
+    fun testToString() {
+        val result = "AdSelectionConfig: seller=$seller, decisionLogicUri='$decisionLogicUri', " +
+            "customAudienceBuyers=$customAudienceBuyers, adSelectionSignals=$adSelectionSignals, " +
+            "sellerSignals=$sellerSignals, perBuyerSignals=$perBuyerSignals, " +
+            "trustedScoringSignalsUri=$trustedScoringSignalsUri"
+        val request = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        var adSelectionConfig2 = AdSelectionConfig(
+            AdTechIdentifier("1234"),
+            Uri.parse("www.abc.com"),
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        Truth.assertThat(adSelectionConfig == adSelectionConfig2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
new file mode 100644
index 0000000..7ce3d77
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManagerTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.adservices.adselection.AdSelectionOutcome
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionManager.Companion.obtain
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+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 kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdSelectionManagerTest {
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdSelectionOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testSelectAds() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            managerCompat!!.selectAds(adSelectionConfig)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.AdSelectionConfig::class.java)
+        verify(adSelectionManager).selectAds(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testReportImpression() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = obtain(mContext)
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.reportImpression(reportImpressionRequest)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.ReportImpressionRequest::class.java)
+        verify(adSelectionManager).reportImpression(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyReportImpressionRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private const val adSelectionId = 1234L
+        private const val adId = "1234"
+        private val seller: AdTechIdentifier = AdTechIdentifier(adId)
+        private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+        private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+        private const val adSelectionSignalsStr = "adSelSignals"
+        private val adSelectionSignals: AdSelectionSignals =
+            AdSelectionSignals(adSelectionSignalsStr)
+        private const val sellerSignalsStr = "sellerSignals"
+        private val sellerSignals: AdSelectionSignals = AdSelectionSignals(sellerSignalsStr)
+        private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+            mutableMapOf(Pair(seller, sellerSignals))
+        private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+        private val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+
+        // Response.
+        private val renderUri = Uri.parse("render-uri.com")
+
+        private fun mockAdSelectionManager(
+            spyContext: Context
+        ): android.adservices.adselection.AdSelectionManager {
+            val adSelectionManager =
+                mock(android.adservices.adselection.AdSelectionManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.adselection.AdSelectionManager::class.java))
+                .thenReturn(adSelectionManager)
+            return adSelectionManager
+        }
+
+        private fun setupAdSelectionResponse(
+            adSelectionManager: android.adservices.adselection.AdSelectionManager
+        ) {
+            // Set up the response that AdSelectionManager will return when the compat code calls
+            // it.
+            val response = AdSelectionOutcome.Builder()
+                .setAdSelectionId(adSelectionId)
+                .setRenderUri(renderUri)
+                .build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<AdSelectionOutcome, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adSelectionManager).selectAds(
+                    any(),
+                    any(),
+                    any()
+                )
+
+            val answer2 = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer2).`when`(adSelectionManager).reportImpression(any(), any(), any())
+        }
+
+        private fun verifyRequest(request: android.adservices.adselection.AdSelectionConfig) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = getPlatformAdSelectionConfig()
+
+            Assert.assertEquals(expectedRequest, request)
+        }
+
+        private fun verifyResponse(
+            outcome: androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+        ) {
+            val expectedOutcome =
+                androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome(
+                    adSelectionId,
+                    renderUri)
+            Assert.assertEquals(expectedOutcome, outcome)
+        }
+
+        private fun getPlatformAdSelectionConfig():
+            android.adservices.adselection.AdSelectionConfig {
+            val adTechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adId)
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(adSelectionSignalsStr))
+                .setCustomAudienceBuyers(listOf(adTechIdentifier))
+                .setDecisionLogicUri(decisionLogicUri)
+                .setPerBuyerSignals(mutableMapOf(Pair(
+                    adTechIdentifier,
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))))
+                .setSeller(adTechIdentifier)
+                .setSellerSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))
+                .setTrustedScoringSignalsUri(trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun verifyReportImpressionRequest(
+            request: android.adservices.adselection.ReportImpressionRequest
+        ) {
+            val expectedRequest = android.adservices.adselection.ReportImpressionRequest(
+                adSelectionId,
+                getPlatformAdSelectionConfig())
+            Assert.assertEquals(expectedRequest.adSelectionId, request.adSelectionId)
+            Assert.assertEquals(expectedRequest.adSelectionConfig, request.adSelectionConfig)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcomeTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcomeTest.kt
new file mode 100644
index 0000000..3d1bf4d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcomeTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdSelectionOutcomeTest {
+    private val adSelectionId = 1234L
+    private val renderUri = Uri.parse("abc.com")
+    @Test
+    fun testToString() {
+        val result = "AdSelectionOutcome: adSelectionId=$adSelectionId, renderUri=$renderUri"
+        val request = AdSelectionOutcome(adSelectionId, renderUri)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adSelectionOutcome = AdSelectionOutcome(adSelectionId, renderUri)
+        var adSelectionOutcome2 = AdSelectionOutcome(adSelectionId, Uri.parse("abc.com"))
+        Truth.assertThat(adSelectionOutcome == adSelectionOutcome2).isTrue()
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequestTest.kt
new file mode 100644
index 0000000..4e9dc25
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequestTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ReportImpressionRequestTest {
+    private val adSelectionId = 1234L
+    private val seller: AdTechIdentifier = AdTechIdentifier("1234")
+    private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+    private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+    private val adSelectionSignals: AdSelectionSignals = AdSelectionSignals("adSelSignals")
+    private val sellerSignals: AdSelectionSignals = AdSelectionSignals("sellerSignals")
+    private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+        mutableMapOf(Pair(seller, sellerSignals))
+    private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+    private val adSelectionConfig = AdSelectionConfig(
+        seller,
+        decisionLogicUri,
+        customAudienceBuyers,
+        adSelectionSignals,
+        sellerSignals,
+        perBuyerSignals,
+        trustedScoringSignalsUri)
+
+    @Test
+    fun testToString() {
+        val result = "ReportImpressionRequest: adSelectionId=$adSelectionId, " +
+            "adSelectionConfig=$adSelectionConfig"
+        val request = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+        var adSelectionConfig2 = AdSelectionConfig(
+            AdTechIdentifier("1234"),
+            Uri.parse("www.abc.com"),
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+        var reportImpressionRequest2 = ReportImpressionRequest(adSelectionId, adSelectionConfig2)
+        Truth.assertThat(reportImpressionRequest == reportImpressionRequest2).isTrue()
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
new file mode 100644
index 0000000..863e9a7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManagerTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+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 kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AppSetIdManagerTest {
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAppSetIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AppSetIdManager.obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAppSetIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val appSetIdManager = mockAppSetIdManager(mContext)
+        setupResponse(appSetIdManager)
+        val managerCompat = AppSetIdManager.obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            managerCompat!!.getAppSetId()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(appSetIdManager).getAppSetId(any(), any())
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAppSetIdManager(
+            spyContext: Context
+        ): android.adservices.appsetid.AppSetIdManager {
+            val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.appsetid.AppSetIdManager::class.java))
+                .thenReturn(appSetIdManager)
+            return appSetIdManager
+        }
+
+        private fun setupResponse(appSetIdManager: android.adservices.appsetid.AppSetIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val appSetId = android.adservices.appsetid.AppSetId(
+                "1234",
+                android.adservices.appsetid.AppSetId.SCOPE_APP)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.appsetid.AppSetId, Exception>>(1)
+                receiver.onResult(appSetId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(appSetIdManager).getAppSetId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(appSetId: AppSetId) {
+            Assert.assertEquals("1234", appSetId.id)
+            Assert.assertEquals(AppSetId.SCOPE_APP, appSetId.scope)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdTest.kt
new file mode 100644
index 0000000..49aa2db
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AppSetIdTest {
+    @Test
+    fun testToString() {
+        val result = "AppSetId: id=1234, scope=SCOPE_DEVELOPER"
+        val id = AppSetId("1234", AppSetId.SCOPE_DEVELOPER)
+        Truth.assertThat(id.toString()).isEqualTo(result)
+
+        val result2 = "AppSetId: id=4321, scope=SCOPE_APP"
+        val id2 = AppSetId("4321", AppSetId.SCOPE_APP)
+        Truth.assertThat(id2.toString()).isEqualTo(result2)
+    }
+
+    @Test
+    fun testEquals() {
+        val id1 = AppSetId("1234", AppSetId.SCOPE_DEVELOPER)
+        val id2 = AppSetId("1234", AppSetId.SCOPE_DEVELOPER)
+        Truth.assertThat(id1 == id2).isTrue()
+
+        val id3 = AppSetId("1234", AppSetId.SCOPE_APP)
+        Truth.assertThat(id1 == id3).isFalse()
+    }
+
+    @Test
+    fun testScopeUndefined() {
+        assertThrows<IllegalArgumentException> {
+            AppSetId("1234", 3 /* Invalid scope */)
+        }.hasMessageThat().contains("Scope undefined.")
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
new file mode 100644
index 0000000..501b15f
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdDataTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdDataTest {
+    private val uri: Uri = Uri.parse("abc.com")
+    private val metadata = "metadata"
+    @Test
+    fun testToString() {
+        val result = "AdData: renderUri=$uri, metadata='$metadata'"
+        val request = AdData(uri, metadata)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val adData1 = AdData(uri, metadata)
+        var adData2 = AdData(Uri.parse("abc.com"), "metadata")
+        Truth.assertThat(adData1 == adData2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignalsTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignalsTest.kt
new file mode 100644
index 0000000..21cdcd13
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignalsTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdSelectionSignalsTest {
+    private val signals = "signals"
+
+    @Test
+    fun testToString() {
+        val result = "AdSelectionSignals: $signals"
+        val request = AdSelectionSignals(signals)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val id1 = AdSelectionSignals(signals)
+        var id2 = AdSelectionSignals("signals")
+        Truth.assertThat(id1 == id2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifierTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifierTest.kt
new file mode 100644
index 0000000..00a98bb
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifierTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AdTechIdentifierTest {
+    private val identifier = "ad-tech-identifier"
+
+    @Test
+    fun testToString() {
+        val result = "AdTechIdentifier: $identifier"
+        val request = AdTechIdentifier(identifier)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val id1 = AdTechIdentifier(identifier)
+        var id2 = AdTechIdentifier("ad-tech-identifier")
+        Truth.assertThat(id1 == id2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
new file mode 100644
index 0000000..0626024
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManagerTest.kt
@@ -0,0 +1,211 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.adservices.customaudience.CustomAudienceManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudienceManager.Companion.obtain
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class CustomAudienceManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testJoinCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            val customAudience = CustomAudience.Builder(buyer, name, uri, uri, ads)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .build()
+            val request = JoinCustomAudienceRequest(customAudience)
+            managerCompat!!.joinCustomAudience(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.JoinCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).joinCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyJoinCustomAudienceRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testLeaveCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            val request = LeaveCustomAudienceRequest(buyer, name)
+            managerCompat!!.leaveCustomAudience(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.LeaveCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).leaveCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyLeaveCustomAudienceRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val uri: Uri = Uri.parse("abc.com")
+        private const val adtech = "1234"
+        private val buyer: AdTechIdentifier = AdTechIdentifier(adtech)
+        private const val name: String = "abc"
+        private const val signals = "signals"
+        private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals(signals)
+        private val keys: List<String> = listOf("key1", "key2")
+        private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+        private const val metadata = "metadata"
+        private val ads: List<AdData> = listOf(AdData(uri, metadata))
+
+        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+            val customAudienceManager = mock(CustomAudienceManager::class.java)
+            `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
+                .thenReturn(customAudienceManager)
+            return customAudienceManager
+        }
+
+        private fun setupResponse(customAudienceManager: CustomAudienceManager) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer).`when`(customAudienceManager).joinCustomAudience(any(), any(), any())
+            doAnswer(answer).`when`(customAudienceManager).leaveCustomAudience(any(), any(), any())
+        }
+
+        private fun verifyJoinCustomAudienceRequest(
+            joinCustomAudienceRequest: android.adservices.customaudience.JoinCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+            val userBiddingSignals =
+                android.adservices.common.AdSelectionSignals.fromString(signals)
+            val trustedBiddingSignals =
+                android.adservices.customaudience.TrustedBiddingData.Builder()
+                .setTrustedBiddingKeys(keys)
+                .setTrustedBiddingUri(uri)
+                .build()
+            val customAudience = android.adservices.customaudience.CustomAudience.Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setBiddingLogicUri(uri)
+                .setDailyUpdateUri(uri)
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .setAds(listOf(android.adservices.common.AdData.Builder()
+                    .setRenderUri(uri)
+                    .setMetadata(metadata)
+                    .build()))
+                .build()
+
+            val expectedRequest =
+                android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                    .setCustomAudience(customAudience)
+                    .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == joinCustomAudienceRequest).isTrue()
+        }
+
+        private fun verifyLeaveCustomAudienceRequest(
+            leaveCustomAudienceRequest: android.adservices.customaudience.LeaveCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+
+            val expectedRequest = android.adservices.customaudience.LeaveCustomAudienceRequest
+                .Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == leaveCustomAudienceRequest).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceTest.kt
new file mode 100644
index 0000000..fa5c6ce
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 26)
+class CustomAudienceTest {
+    private val uri: Uri = Uri.parse("abc.com")
+    private val buyer: AdTechIdentifier = AdTechIdentifier("1234")
+    private val name: String = "abc"
+    private val activationTime: Instant = Instant.now()
+    private val expirationTime: Instant = Instant.now()
+    private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals("signals")
+    private val keys: List<String> = listOf("key1", "key2")
+    private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+    private val ads: List<AdData> = listOf(AdData(uri, "metadata"))
+
+    @Test
+    fun testToStringAndEquals() {
+        val result = "CustomAudience: buyer=abc.com, activationTime=$activationTime, " +
+            "expirationTime=$expirationTime, dailyUpdateUri=abc.com, " +
+            "userBiddingSignals=AdSelectionSignals: signals, " +
+            "trustedBiddingSignals=TrustedBiddingData: trustedBiddingUri=abc.com " +
+            "trustedBiddingKeys=[key1, key2], biddingLogicUri=abc.com, " +
+            "ads=[AdData: renderUri=abc.com, metadata='metadata']"
+
+        val customAudience = CustomAudience(
+            buyer,
+            name,
+            uri,
+            uri,
+            ads,
+            activationTime,
+            expirationTime,
+            userBiddingSignals,
+            trustedBiddingSignals)
+        Truth.assertThat(customAudience.toString()).isEqualTo(result)
+
+        // Verify Builder.
+        val customAudienceBuilder2 = CustomAudience.Builder(buyer, name, uri, uri, ads)
+            .setActivationTime(activationTime)
+            .setExpirationTime(expirationTime)
+            .setUserBiddingSignals(userBiddingSignals)
+            .setTrustedBiddingData(trustedBiddingSignals)
+        Truth.assertThat(customAudienceBuilder2.build().toString()).isEqualTo(result)
+
+        // Test equality.
+        Truth.assertThat(customAudience == customAudienceBuilder2.build()).isTrue()
+
+        // Reset values of Builder.
+        customAudienceBuilder2.setName("newName")
+        Truth.assertThat(customAudience == customAudienceBuilder2.build()).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequestTest.kt
new file mode 100644
index 0000000..7638a70
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequestTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 26)
+class JoinCustomAudienceRequestTest {
+    private val uri: Uri = Uri.parse("abc.com")
+    private val buyer: AdTechIdentifier = AdTechIdentifier("1234")
+    private val name: String = "abc"
+    private val activationTime: Instant = Instant.now()
+    private val expirationTime: Instant = Instant.now()
+    private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals("signals")
+    private val keys: List<String> = listOf("key1", "key2")
+    private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+    private val ads: List<AdData> = listOf(AdData(uri, "metadata"))
+
+    @Test
+    fun testToString() {
+        val customAudience = CustomAudience(
+            buyer,
+            name,
+            uri,
+            uri,
+            ads,
+            activationTime,
+            expirationTime,
+            userBiddingSignals,
+            trustedBiddingSignals)
+        val result = "JoinCustomAudience: customAudience=$customAudience"
+        val joinCustomAudienceRequest = JoinCustomAudienceRequest(customAudience)
+        Truth.assertThat(joinCustomAudienceRequest.toString()).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceTest.kt
new file mode 100644
index 0000000..409f780
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceTest.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LeaveCustomAudienceTest {
+    private val adTechIdentifier: AdTechIdentifier = AdTechIdentifier("1234")
+    private val name = "abc"
+    @Test
+    fun testToString() {
+        val result = "LeaveCustomAudience: buyer=AdTechIdentifier: 1234, name=abc"
+        val leaveCustomAudienceRequest = LeaveCustomAudienceRequest(adTechIdentifier, name)
+        Truth.assertThat(leaveCustomAudienceRequest.toString()).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
new file mode 100644
index 0000000..1476dae
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingDataTest.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TrustedBiddingDataTest {
+    private val uri = Uri.parse("abc.com")
+    private val keys = listOf("key1", "key2")
+    @Test
+    fun testToString() {
+        val result = "TrustedBiddingData: trustedBiddingUri=abc.com trustedBiddingKeys=[key1, key2]"
+        val trustedBiddingData = TrustedBiddingData(uri, keys)
+        Truth.assertThat(trustedBiddingData.toString()).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequestTest.kt
new file mode 100644
index 0000000..e9b83d7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequestTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class DeletionRequestTest {
+    @Test
+    fun testToString() {
+        val now = Instant.now()
+        val result = "DeletionRequest { DeletionMode=DELETION_MODE_ALL, " +
+            "MatchBehavior=MATCH_BEHAVIOR_DELETE, " +
+            "Start=$now, End=$now, DomainUris=[www.abc.com], OriginUris=[www.xyz.com] }"
+
+        val deletionRequest = DeletionRequest(
+            DeletionRequest.DELETION_MODE_ALL,
+            DeletionRequest.MATCH_BEHAVIOR_DELETE,
+            now,
+            now,
+            listOf(Uri.parse("www.abc.com")),
+            listOf(Uri.parse("www.xyz.com")),
+        )
+        Truth.assertThat(deletionRequest.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val deletionRequest1 = DeletionRequest(
+            DeletionRequest.DELETION_MODE_ALL,
+            DeletionRequest.MATCH_BEHAVIOR_DELETE,
+            Instant.MIN,
+            Instant.MAX,
+            listOf(Uri.parse("www.abc.com")),
+            listOf(Uri.parse("www.xyz.com")))
+        val deletionRequest2 = DeletionRequest.Builder(
+            deletionMode = DeletionRequest.DELETION_MODE_ALL,
+            matchBehavior = DeletionRequest.MATCH_BEHAVIOR_DELETE)
+            .setDomainUris(listOf(Uri.parse("www.abc.com")))
+            .setOriginUris(listOf(Uri.parse("www.xyz.com")))
+            .build()
+        Truth.assertThat(deletionRequest1 == deletionRequest2).isTrue()
+    }
+
+    @Test
+    fun testIllegalArguments() {
+        assertThrows<IllegalArgumentException> {
+            DeletionRequest(
+                2 /* Invalid deletionMode */,
+                DeletionRequest.MATCH_BEHAVIOR_DELETE,
+                Instant.MIN,
+                Instant.MAX,
+                listOf(Uri.parse("www.abc.com")),
+                listOf(Uri.parse("www.xyz.com")))
+        }.hasMessageThat().contains("DeletionMode undefined.")
+
+        assertThrows<IllegalArgumentException> {
+            DeletionRequest(
+                DeletionRequest.DELETION_MODE_ALL,
+                2 /* Invalid matchBehavior */,
+                Instant.MIN,
+                Instant.MAX,
+                listOf(Uri.parse("www.abc.com")),
+                listOf(Uri.parse("www.xyz.com")))
+        }.hasMessageThat().contains("MatchBehavior undefined.")
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
new file mode 100644
index 0000000..0d05aa5
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManagerTest.kt
@@ -0,0 +1,336 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.adservices.measurement.MeasurementManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.measurement.MeasurementManager.Companion.obtain
+import androidx.test.core.app.ApplicationProvider
+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 java.time.Instant
+import kotlinx.coroutines.runBlocking
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class MeasurementManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testMeasurementOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testDeleteRegistrations() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+
+        // Set up the request.
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).deleteRegistrations(any(), any(), any())
+
+        // Actually invoke the compat code.
+        runBlocking {
+            val request = DeletionRequest(
+                DeletionRequest.DELETION_MODE_ALL,
+                DeletionRequest.MATCH_BEHAVIOR_DELETE,
+                Instant.now(),
+                Instant.now(),
+                listOf(uri1),
+                listOf(uri1))
+
+            managerCompat!!.deleteRegistrations(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.measurement.DeletionRequest::class.java
+        )
+        verify(measurementManager).deleteRegistrations(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyDeletionRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterSource() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val inputEvent = mock(InputEvent::class.java)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(3)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerSource(any(), any(), any(), any())
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerSource(uri1, inputEvent)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        val captor2 = ArgumentCaptor.forClass(InputEvent::class.java)
+        verify(measurementManager).registerSource(
+            captor1.capture(),
+            captor2.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value == uri1)
+        assertThat(captor2.value == inputEvent)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterTrigger() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerTrigger(any(), any(), any())
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerTrigger(uri1)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(Uri::class.java)
+        verify(measurementManager).registerTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(captor1.value).isEqualTo(uri1)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebSource() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebSource(any(), any(), any())
+
+        val request = WebSourceRegistrationRequest.Builder(
+            listOf(WebSourceParams(uri1, false)), uri1)
+            .setAppDestination(uri1)
+            .build()
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerWebSource(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebSourceRegistrationRequest::class.java)
+        verify(measurementManager).registerWebSource(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.topOriginUri == uri1)
+        assertThat(actualRequest.sourceParams.size == 1)
+        assertThat(actualRequest.sourceParams[0].registrationUri == uri1)
+        assertThat(!actualRequest.sourceParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testRegisterWebTrigger() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+            receiver.onResult(Object())
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).registerWebTrigger(any(), any(), any())
+
+        val request = WebTriggerRegistrationRequest(
+            listOf(WebTriggerParams(uri1, false)), uri2)
+
+        // Actually invoke the compat code.
+        runBlocking {
+            managerCompat!!.registerWebTrigger(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor1 = ArgumentCaptor.forClass(
+            android.adservices.measurement.WebTriggerRegistrationRequest::class.java)
+        verify(measurementManager).registerWebTrigger(
+            captor1.capture(),
+            any(),
+            any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        val actualRequest = captor1.value
+        assertThat(actualRequest.destination).isEqualTo(uri2)
+        assertThat(actualRequest.triggerParams.size == 1)
+        assertThat(actualRequest.triggerParams[0].registrationUri == uri1)
+        assertThat(!actualRequest.triggerParams[0].isDebugKeyAllowed)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testMeasurementApiStatus() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val state = MeasurementManager.MEASUREMENT_API_STATE_ENABLED
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+            receiver.onResult(state)
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Actually invoke the compat code.
+        val actualResult = runBlocking {
+            managerCompat!!.getMeasurementApiStatus()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        assertThat(actualResult == state)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testMeasurementApiStatusUnknown() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val measurementManager = mockMeasurementManager(mContext)
+        val managerCompat = obtain(mContext)
+        val answer = { args: InvocationOnMock ->
+            val receiver = args.getArgument<OutcomeReceiver<Int, Exception>>(1)
+            receiver.onResult(5 /* Greater than values returned in SdkExtensions.AD_SERVICES = 4 */)
+            null
+        }
+        doAnswer(answer).`when`(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Actually invoke the compat code.
+        val actualResult = runBlocking {
+            managerCompat!!.getMeasurementApiStatus()
+        }
+
+        // Verify that the compat code was invoked correctly.
+        verify(measurementManager).getMeasurementApiStatus(any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        // Since the compat code does not know the returned state, it sets it to UNKNOWN.
+        assertThat(actualResult == 5)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+
+        private val uri1: Uri = Uri.parse("www.abc.com")
+        private val uri2: Uri = Uri.parse("http://www.xyz.com")
+
+        private lateinit var mContext: Context
+
+        private fun mockMeasurementManager(spyContext: Context): MeasurementManager {
+            val measurementManager = mock(MeasurementManager::class.java)
+            `when`(spyContext.getSystemService(MeasurementManager::class.java))
+                .thenReturn(measurementManager)
+            return measurementManager
+        }
+
+        private fun verifyDeletionRequest(request: android.adservices.measurement.DeletionRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.measurement.DeletionRequest.Builder()
+                .setDomainUris(listOf(uri1))
+                .setOriginUris(listOf(uri1))
+                .build()
+
+            assertThat(HashSet(request.domainUris) == HashSet(expectedRequest.domainUris))
+            assertThat(HashSet(request.originUris) == HashSet(expectedRequest.originUris))
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParamsTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParamsTest.kt
new file mode 100644
index 0000000..4850355
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParamsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebSourceParamsTest {
+    @Test
+    fun testToString() {
+        val result = "WebSourceParams { RegistrationUri=www.abc.com, DebugKeyAllowed=false }"
+
+        val request = WebSourceParams(Uri.parse("www.abc.com"), false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val request1 = WebSourceParams(Uri.parse("www.abc.com"), false)
+        val request2 = WebSourceParams(Uri.parse("www.abc.com"), false)
+        val request3 = WebSourceParams(Uri.parse("https://abc.com"), false)
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 == request3).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequestTest.kt
new file mode 100644
index 0000000..a404850
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequestTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebSourceRegistrationRequestTest {
+    @Test
+    fun testToString() {
+        val result = "WebSourceRegistrationRequest { WebSourceParams=" +
+            "[[WebSourceParams { RegistrationUri=www.abc.com, DebugKeyAllowed=false }]], " +
+            "TopOriginUri=www.abc.com, InputEvent=null, AppDestination=null, WebDestination=null," +
+            " VerifiedDestination=null }"
+
+        val uri = Uri.parse("www.abc.com")
+        val params = listOf(WebSourceParams(uri, false))
+        val request = WebSourceRegistrationRequest.Builder(params, uri).build()
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        val result2 = "WebSourceRegistrationRequest { WebSourceParams=[[WebSourceParams " +
+            "{ RegistrationUri=www.abc.com, DebugKeyAllowed=false }]], TopOriginUri=www.abc.com, " +
+            "InputEvent=null, AppDestination=www.abc.com, WebDestination=www.abc.com, " +
+            "VerifiedDestination=www.abc.com }"
+
+        val params2 = listOf(WebSourceParams(uri, false))
+        val request2 = WebSourceRegistrationRequest.Builder(params2, uri)
+            .setWebDestination(uri)
+            .setAppDestination(uri)
+            .setVerifiedDestination(uri)
+            .build()
+        Truth.assertThat(request2.toString()).isEqualTo(result2)
+    }
+
+    @Test
+    fun testEquals() {
+        val uri = Uri.parse("www.abc.com")
+
+        val params = listOf(WebSourceParams(uri, false))
+        val request1 = WebSourceRegistrationRequest.Builder(params, uri)
+            .setWebDestination(uri)
+            .setAppDestination(uri)
+            .setVerifiedDestination(uri)
+            .build()
+        val request2 = WebSourceRegistrationRequest(
+            params,
+            uri,
+            null,
+            uri,
+            uri,
+            uri)
+        val request3 = WebSourceRegistrationRequest.Builder(params, uri).build()
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 != request3).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParamsTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParamsTest.kt
new file mode 100644
index 0000000..677e163
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParamsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebTriggerParamsTest {
+    @Test
+    fun testToString() {
+        val result = "WebTriggerParams { RegistrationUri=www.abc.com, DebugKeyAllowed=false }"
+
+        val request = WebTriggerParams(Uri.parse("www.abc.com"), false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val request1 = WebTriggerParams(Uri.parse("www.abc.com"), false)
+        val request2 = WebTriggerParams(Uri.parse("www.abc.com"), false)
+        val request3 = WebTriggerParams(Uri.parse("https://abc.com"), false)
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 == request3).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequestTest.kt
new file mode 100644
index 0000000..2f64489
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequestTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 33)
+class WebTriggerRegistrationRequestTest {
+    @Test
+    fun testToString() {
+        val result = "WebTriggerRegistrationRequest { WebTriggerParams=[WebTriggerParams " +
+            "{ RegistrationUri=www.abc.com, DebugKeyAllowed=false }], Destination=www.abc.com"
+
+        val uri = Uri.parse("www.abc.com")
+        val params = listOf(WebTriggerParams(uri, false))
+        val request = WebTriggerRegistrationRequest(params, uri)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val uri = Uri.parse("www.abc.com")
+
+        val params = listOf(WebTriggerParams(uri, false))
+        val request1 = WebTriggerRegistrationRequest(params, uri)
+        val request2 = WebTriggerRegistrationRequest(params, uri)
+        val request3 = WebTriggerRegistrationRequest(
+            params,
+            Uri.parse("https://abc.com"))
+
+        Truth.assertThat(request1 == request2).isTrue()
+        Truth.assertThat(request1 != request3).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
new file mode 100644
index 0000000..a04b48d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequestTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GetTopicsRequestTest {
+    @Test
+    fun testToString() {
+        val result = "GetTopicsRequest: sdkName=sdk1, shouldRecordObservation=false"
+        val request = GetTopicsRequest("sdk1", false)
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        // Verify Builder.
+        val request2 = GetTopicsRequest.Builder()
+            .setSdkName("sdk1")
+            .setShouldRecordObservation(false)
+            .build()
+        Truth.assertThat(request.toString()).isEqualTo(result)
+
+        // Verify equality.
+        Truth.assertThat(request == request2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseTest.kt
new file mode 100644
index 0000000..1078022
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponseTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GetTopicsResponseTest {
+    @Test
+    fun testToString() {
+        val topicsString = "Topic { TaxonomyVersion=1, ModelVersion=10, TopicCode=100 }, " +
+            "Topic { TaxonomyVersion=2, ModelVersion=20, TopicCode=200 }"
+        val result = "Topics=[$topicsString]"
+
+        val topic1 = Topic(1, 10, 100)
+        var topic2 = Topic(2, 20, 200)
+        val response1 = GetTopicsResponse(listOf(topic1, topic2))
+        Truth.assertThat(response1.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val topic1 = Topic(1, 10, 100)
+        var topic2 = Topic(2, 20, 200)
+        val response1 = GetTopicsResponse(listOf(topic1, topic2))
+        val response2 = GetTopicsResponse(listOf(topic2, topic1))
+        Truth.assertThat(response1 == response2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicTest.kt
new file mode 100644
index 0000000..baac9ff
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicTest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TopicTest {
+    @Test
+    fun testToString() {
+        val result = "Topic { TaxonomyVersion=1, ModelVersion=10, TopicCode=100 }"
+        val topic = Topic(/* taxonomyVersion= */ 1, /* modelVersion= */ 10, /* topicId= */ 100)
+        Truth.assertThat(topic.toString()).isEqualTo(result)
+    }
+
+    @Test
+    fun testEquals() {
+        val topic1 = Topic(/* taxonomyVersion= */ 1, /* modelVersion= */ 10, /* topicId= */ 100)
+        val topic2 = Topic(/* taxonomyVersion= */ 1, /* modelVersion= */ 10, /* topicId= */ 100)
+        Truth.assertThat(topic1 == topic2).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
new file mode 100644
index 0000000..3619d38
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/androidTest/java/androidx/privacysandbox/ads/adservices/topics/TopicsManagerTest.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import android.adservices.topics.Topic
+import android.adservices.topics.TopicsManager
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.topics.TopicsManager.Companion.obtain
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.lang.IllegalArgumentException
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class TopicsManagerTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testTopicsOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(obtain(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testTopicsAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val topicsManager = mockTopicsManager(mContext)
+        setupTopicsResponse(topicsManager)
+        val managerCompat = obtain(mContext)
+
+        // Actually invoke the compat code.
+        val result = runBlocking {
+            val request = GetTopicsRequest.Builder()
+                .setSdkName(mSdkName)
+                .setShouldRecordObservation(true)
+                .build()
+
+            managerCompat!!.getTopics(request)
+        }
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(android.adservices.topics.GetTopicsRequest::class.java)
+        verify(topicsManager).getTopics(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testTopicsAsyncPreviewNotSupported() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val topicsManager = mockTopicsManager(mContext)
+        setupTopicsResponse(topicsManager)
+        val managerCompat = obtain(mContext)
+
+        val request = GetTopicsRequest.Builder()
+            .setSdkName(mSdkName)
+            .setShouldRecordObservation(false)
+            .build()
+
+        // Actually invoke the compat code.
+        assertThrows<IllegalArgumentException> {
+            runBlocking {
+                managerCompat!!.getTopics(request)
+            }
+        }.hasMessageThat().contains("shouldRecordObservation not supported yet.")
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val mSdkName: String = "sdk1"
+
+        private fun mockTopicsManager(spyContext: Context): TopicsManager {
+            val topicsManager = mock(TopicsManager::class.java)
+            `when`(spyContext.getSystemService(TopicsManager::class.java))
+                .thenReturn(topicsManager)
+            return topicsManager
+        }
+
+        private fun setupTopicsResponse(topicsManager: TopicsManager) {
+            // Set up the response that TopicsManager will return when the compat code calls it.
+            val topic1 = Topic(1, 1, 1)
+            val topic2 = Topic(2, 2, 2)
+            val topics = listOf(topic1, topic2)
+            val response = android.adservices.topics.GetTopicsResponse.Builder(topics).build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.topics.GetTopicsResponse, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(topicsManager).getTopics(
+                    any(),
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyRequest(topicsRequest: android.adservices.topics.GetTopicsRequest) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = android.adservices.topics.GetTopicsRequest.Builder()
+                .setAdsSdkName(mSdkName)
+                .build()
+
+            Assert.assertEquals(expectedRequest.adsSdkName, topicsRequest.adsSdkName)
+        }
+
+        private fun verifyResponse(getTopicsResponse: GetTopicsResponse) {
+            Assert.assertEquals(2, getTopicsResponse.topics.size)
+            val topic1 = getTopicsResponse.topics[0]
+            val topic2 = getTopicsResponse.topics[1]
+            Assert.assertEquals(1, topic1.topicId)
+            Assert.assertEquals(1, topic1.modelVersion)
+            Assert.assertEquals(1, topic1.taxonomyVersion)
+            Assert.assertEquals(2, topic2.topicId)
+            Assert.assertEquals(2, topic2.modelVersion)
+            Assert.assertEquals(2, topic2.taxonomyVersion)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdId.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdId.kt
new file mode 100644
index 0000000..ad9ba22
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdId.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+/**
+ * A unique, user-resettable, device-wide, per-profile ID for advertising as returned by the
+ * [AdIdManager#getAdId()] API.
+ *
+ * Ad networks may use {@code AdId} to monetize for Interest Based Advertising (IBA), i.e.
+ * targeting and remarketing ads. The user may limit availability of this identifier.
+ *
+ * @param adId The advertising ID.
+ * @param isLimitAdTrackingEnabled the limit ad tracking enabled setting.
+ */
+class AdId internal constructor(
+    val adId: String,
+    val isLimitAdTrackingEnabled: Boolean = false
+) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdId) return false
+        return this.adId == other.adId &&
+            this.isLimitAdTrackingEnabled == other.isLimitAdTrackingEnabled
+    }
+
+    override fun hashCode(): Int {
+        var hash = adId.hashCode()
+        hash = 31 * hash + isLimitAdTrackingEnabled.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "AdId: adId=$adId, isLimitAdTrackingEnabled=$isLimitAdTrackingEnabled"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
new file mode 100644
index 0000000..ef41c4d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adid/AdIdManager.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.privacysandbox.ads.adservices.adid
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.ext.SdkExtensions
+import android.os.LimitExceededException
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * AdId Manager provides APIs for app and ad-SDKs to access advertising ID. The advertising ID is a
+ * unique, per-device, user-resettable ID for advertising. It gives users better controls and
+ * provides developers with a simple, standard system to continue to monetize their apps via
+ * personalized ads (formerly known as interest-based ads).
+ */
+abstract class AdIdManager internal constructor() {
+    /**
+     * Return the AdId.
+     *
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+    abstract suspend fun getAdId(): AdId
+
+    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mAdIdManager: android.adservices.adid.AdIdManager
+    ) : AdIdManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.adid.AdIdManager>(
+                android.adservices.adid.AdIdManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+        override suspend fun getAdId(): AdId {
+            return convertResponse(getAdIdAsyncInternal())
+        }
+
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_AD_ID)
+        private suspend fun
+            getAdIdAsyncInternal(): android.adservices.adid.AdId = suspendCancellableCoroutine {
+                continuation ->
+            mAdIdManager.getAdId(
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+
+        private fun convertResponse(response: android.adservices.adid.AdId): AdId {
+            return AdId(response.adId, response.isLimitAdTrackingEnabled)
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdIdManager].
+         *
+         *  @return AdIdManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): AdIdManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                // TODO(b/261770989): Extend this to older versions.
+                null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
new file mode 100644
index 0000000..839b734
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionConfig.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+
+/**
+ * Contains the configuration of the ad selection process.
+ *
+ * Instances of this class are created by SDKs to be provided as arguments to the
+ * [AdSelectionManager#selectAds] and [AdSelectionManager#reportImpression] methods in
+ * [AdSelectionManager].
+ *
+ * @param seller AdTechIdentifier of the seller, for example "www.example-ssp.com".
+ * @param decisionLogicUri the URI used to retrieve the JS code containing the seller/SSP scoreAd
+ *     function used during the ad selection and reporting processes.
+ * @param customAudienceBuyers a list of custom audience buyers allowed by the SSP to participate
+ *     in the ad selection process.
+ * @param adSelectionSignals signals given to the participating buyers in the ad selection and
+ *     reporting processes.
+ * @param sellerSignals represents any information that the SSP used in the ad
+ *     scoring process to tweak the results of the ad selection process (e.g. brand safety
+ *     checks, excluded contextual ads).
+ * @param perBuyerSignals any information that each buyer would provide during ad selection to
+ *     participants (such as bid floor, ad selection type, etc.)
+ * @param trustedScoringSignalsUri URI endpoint of sell-side trusted signal from which creative
+ *     specific realtime information can be fetched from.
+ */
+class AdSelectionConfig public constructor(
+    val seller: AdTechIdentifier,
+    val decisionLogicUri: Uri,
+    val customAudienceBuyers: List<AdTechIdentifier>,
+    val adSelectionSignals: AdSelectionSignals,
+    val sellerSignals: AdSelectionSignals,
+    val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals>,
+    val trustedScoringSignalsUri: Uri
+) {
+
+    /** Checks whether two [AdSelectionConfig] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdSelectionConfig) return false
+        return this.seller == other.seller &&
+            this.decisionLogicUri == other.decisionLogicUri &&
+            this.customAudienceBuyers == other.customAudienceBuyers &&
+            this.adSelectionSignals == other.adSelectionSignals &&
+            this.sellerSignals == other.sellerSignals &&
+            this.perBuyerSignals == other.perBuyerSignals &&
+            this.trustedScoringSignalsUri == other.trustedScoringSignalsUri
+    }
+
+    /** Returns the hash of the [AdSelectionConfig] object's data.  */
+    override fun hashCode(): Int {
+        var hash = seller.hashCode()
+        hash = 31 * hash + decisionLogicUri.hashCode()
+        hash = 31 * hash + customAudienceBuyers.hashCode()
+        hash = 31 * hash + adSelectionSignals.hashCode()
+        hash = 31 * hash + sellerSignals.hashCode()
+        hash = 31 * hash + perBuyerSignals.hashCode()
+        hash = 31 * hash + trustedScoringSignalsUri.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "AdSelectionConfig: seller=$seller, decisionLogicUri='$decisionLogicUri', " +
+            "customAudienceBuyers=$customAudienceBuyers, adSelectionSignals=$adSelectionSignals, " +
+            "sellerSignals=$sellerSignals, perBuyerSignals=$perBuyerSignals, " +
+            "trustedScoringSignalsUri=$trustedScoringSignalsUri"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
new file mode 100644
index 0000000..454a7cb
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionManager.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.TransactionTooLargeException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * AdSelection Manager provides APIs for app and ad-SDKs to run ad selection processes as well
+ * as report impressions.
+ */
+abstract class AdSelectionManager internal constructor() {
+    /**
+     * Runs the ad selection process on device to select a remarketing ad for the caller
+     * application.
+     *
+     * @param adSelectionConfig the config The input {@code adSelectionConfig} is provided by the
+     * Ads SDK and the [AdSelectionConfig] object is transferred via a Binder call. For this
+     * reason, the total size of these objects is bound to the Android IPC limitations. Failures to
+     * transfer the [AdSelectionConfig] will throws an [TransactionTooLargeException].
+     *
+     * The output is passed by the receiver, which either returns an [AdSelectionOutcome]
+     * for a successful run, or an [Exception] includes the type of the exception thrown and
+     * the corresponding error message.
+     *
+     * If the [IllegalArgumentException] is thrown, it is caused by invalid input argument
+     * the API received to run the ad selection.
+     *
+     * If the [IllegalStateException] is thrown with error message "Failure of AdSelection
+     * services.", it is caused by an internal failure of the ad selection service.
+     *
+     * If the [TimeoutException] is thrown, it is caused when a timeout is encountered
+     * during bidding, scoring, or overall selection process to find winning Ad.
+     *
+     * If the [LimitExceededException] is thrown, it is caused when the calling package
+     * exceeds the allowed rate limits and is throttled.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun selectAds(adSelectionConfig: AdSelectionConfig): AdSelectionOutcome
+
+    /**
+     * Report the given impression. The [ReportImpressionRequest] is provided by the Ads SDK.
+     * The receiver either returns a {@code void} for a successful run, or an [Exception]
+     * indicates the error.
+     *
+     * @param reportImpressionRequest the request for reporting impression.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest)
+
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mAdSelectionManager: android.adservices.adselection.AdSelectionManager
+    ) : AdSelectionManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.adselection.AdSelectionManager>(
+                android.adservices.adselection.AdSelectionManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun selectAds(adSelectionConfig: AdSelectionConfig): AdSelectionOutcome {
+            return convertResponse(selectAdsInternal(convertAdSelectionConfig(adSelectionConfig)))
+        }
+
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        private suspend fun selectAdsInternal(
+            adSelectionConfig: android.adservices.adselection.AdSelectionConfig
+        ): android.adservices.adselection.AdSelectionOutcome = suspendCancellableCoroutine { cont
+            ->
+            mAdSelectionManager.selectAds(
+                adSelectionConfig,
+                Runnable::run,
+                cont.asOutcomeReceiver()
+            )
+        }
+
+        private fun convertAdSelectionConfig(
+            request: AdSelectionConfig
+        ): android.adservices.adselection.AdSelectionConfig {
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(convertAdSelectionSignals(request.adSelectionSignals))
+                .setCustomAudienceBuyers(convertBuyers(request.customAudienceBuyers))
+                .setDecisionLogicUri(request.decisionLogicUri)
+                .setSeller(android.adservices.common.AdTechIdentifier.fromString(
+                    request.seller.identifier))
+                .setPerBuyerSignals(convertPerBuyerSignals(request.perBuyerSignals))
+                .setSellerSignals(convertAdSelectionSignals(request.sellerSignals))
+                .setTrustedScoringSignalsUri(request.trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun convertAdSelectionSignals(
+            request: AdSelectionSignals
+        ): android.adservices.common.AdSelectionSignals {
+            return android.adservices.common.AdSelectionSignals.fromString(request.signals)
+        }
+
+        private fun convertBuyers(
+            buyers: List<AdTechIdentifier>
+        ): MutableList<android.adservices.common.AdTechIdentifier> {
+            var ids = mutableListOf<android.adservices.common.AdTechIdentifier>()
+            for (buyer in buyers) {
+                ids.add(android.adservices.common.AdTechIdentifier.fromString(buyer.identifier))
+            }
+            return ids
+        }
+
+        private fun convertPerBuyerSignals(
+            request: Map<AdTechIdentifier, AdSelectionSignals>
+        ): Map<android.adservices.common.AdTechIdentifier,
+            android.adservices.common.AdSelectionSignals?> {
+            var map = HashMap<android.adservices.common.AdTechIdentifier,
+                android.adservices.common.AdSelectionSignals?>()
+            for (key in request.keys) {
+                val id = android.adservices.common.AdTechIdentifier.fromString(key.identifier)
+                val value = if (request[key] != null) convertAdSelectionSignals(request[key]!!)
+                    else null
+                map[id] = value
+            }
+            return map
+        }
+
+        private fun convertResponse(
+            response: android.adservices.adselection.AdSelectionOutcome
+        ): AdSelectionOutcome {
+            return AdSelectionOutcome(response.adSelectionId, response.renderUri)
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun reportImpression(reportImpressionRequest: ReportImpressionRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mAdSelectionManager.reportImpression(
+                    convertReportImpressionRequest(reportImpressionRequest),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        private fun convertReportImpressionRequest(
+            request: ReportImpressionRequest
+        ): android.adservices.adselection.ReportImpressionRequest {
+            return android.adservices.adselection.ReportImpressionRequest(
+                request.adSelectionId,
+                convertAdSelectionConfig(request.adSelectionConfig)
+            )
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [AdSelectionManager].
+         *
+         *  @return AdSelectionManager object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): AdSelectionManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
new file mode 100644
index 0000000..9286e12
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/AdSelectionOutcome.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+import android.net.Uri
+
+/**
+ * This class represents  input to the [AdSelectionManager#selectAds] in the
+ * [AdSelectionManager]. This field is populated in the case of a successful
+ * [AdSelectionManager#selectAds] call.
+ *
+ * @param adSelectionId An ID unique only to a device user that identifies a successful ad
+ *     selection.
+ * @param renderUri A render URL for the winning ad.
+ */
+class AdSelectionOutcome public constructor(
+    val adSelectionId: Long,
+    val renderUri: Uri
+) {
+
+    /** Checks whether two [AdSelectionOutcome] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdSelectionOutcome) return false
+        return this.adSelectionId == other.adSelectionId &&
+            this.renderUri == other.renderUri
+    }
+
+    /** Returns the hash of the [AdSelectionOutcome] object's data.  */
+    override fun hashCode(): Int {
+        var hash = adSelectionId.hashCode()
+        hash = 31 * hash + renderUri.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "AdSelectionOutcome: adSelectionId=$adSelectionId, renderUri=$renderUri"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
new file mode 100644
index 0000000..3740b5d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/adselection/ReportImpressionRequest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.privacysandbox.ads.adservices.adselection
+
+/**
+ * Represent input parameters to the reportImpression API.
+ *
+ * @param adSelectionId An ID unique only to a device user that identifies a successful ad
+ *     selection.
+ * @param adSelectionConfig The same configuration used in the selectAds() call identified by the
+ *      provided ad selection ID.
+ */
+class ReportImpressionRequest public constructor(
+    val adSelectionId: Long,
+    val adSelectionConfig: AdSelectionConfig
+) {
+
+    /** Checks whether two [ReportImpressionRequest] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ReportImpressionRequest) return false
+        return this.adSelectionId == other.adSelectionId &&
+            this.adSelectionConfig == other.adSelectionConfig
+    }
+
+    /** Returns the hash of the [ReportImpressionRequest] object's data.  */
+    override fun hashCode(): Int {
+        var hash = adSelectionId.hashCode()
+        hash = 31 * hash + adSelectionConfig.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "ReportImpressionRequest: adSelectionId=$adSelectionId, " +
+            "adSelectionConfig=$adSelectionConfig"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetId.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetId.kt
new file mode 100644
index 0000000..516ba9e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetId.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+/**
+ * A unique, per-device, per developer-account user-resettable ID for non-monetizing advertising
+ * use cases.
+ *
+ * Represents the appSetID and scope of this appSetId from the
+ * [AppSetIdManager#getAppSetId()] API. The scope of the ID can be per app or per developer account
+ * associated with the user. AppSetId is used for analytics, spam detection, frequency capping and
+ * fraud prevention use cases, on a given device, that one may need to correlate usage or actions
+ * across a set of apps owned by an organization.
+ *
+ * @param id The appSetID.
+ * @param scope The scope of the ID. Can be AppSetId.SCOPE_APP or AppSetId.SCOPE_DEVELOPER.
+ */
+class AppSetId public constructor(
+    val id: String,
+    val scope: Int
+) {
+    init {
+        require(scope == SCOPE_APP || scope == SCOPE_DEVELOPER) { "Scope undefined." }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AppSetId) return false
+        return this.id == other.id &&
+            this.scope == other.scope
+    }
+
+    override fun hashCode(): Int {
+        var hash = id.hashCode()
+        hash = 31 * hash + scope.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        var scopeStr = if (scope == 1) "SCOPE_APP" else "SCOPE_DEVELOPER"
+        return "AppSetId: id=$id, scope=$scopeStr"
+    }
+
+    companion object {
+        /**
+         * The appSetId is scoped to an app. All apps on a device will have a different appSetId.
+         */
+        public const val SCOPE_APP = 1
+
+        /**
+         * The appSetId is scoped to a developer account on an app store. All apps from the same
+         * developer on a device will have the same developer scoped appSetId.
+         */
+        public const val SCOPE_DEVELOPER = 2
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
new file mode 100644
index 0000000..844de9a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/appsetid/AppSetIdManager.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.privacysandbox.ads.adservices.appsetid
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * AppSetIdManager provides APIs for app and ad-SDKs to access appSetId for non-monetizing purpose.
+ */
+abstract class AppSetIdManager internal constructor() {
+    /**
+     * Retrieve the AppSetId.
+     *
+     * @throws [SecurityException] if caller is not authorized to call this API.
+     * @throws [IllegalStateException] if this API is not available.
+     * @throws [LimitExceededException] if rate limit was reached.
+     */
+    abstract suspend fun getAppSetId(): AppSetId
+
+    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mAppSetIdManager: android.adservices.appsetid.AppSetIdManager
+    ) : AppSetIdManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.appsetid.AppSetIdManager>(
+                android.adservices.appsetid.AppSetIdManager::class.java
+            )
+        )
+
+        @DoNotInline
+        override suspend fun getAppSetId(): AppSetId {
+            return convertResponse(getAppSetIdAsyncInternal())
+        }
+
+        private suspend fun getAppSetIdAsyncInternal(): android.adservices.appsetid.AppSetId =
+            suspendCancellableCoroutine {
+                    continuation ->
+                mAppSetIdManager.getAppSetId(
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+
+        private fun convertResponse(response: android.adservices.appsetid.AppSetId): AppSetId {
+            if (response.scope == android.adservices.appsetid.AppSetId.SCOPE_APP) {
+                return AppSetId(response.id, AppSetId.SCOPE_APP)
+            }
+            return AppSetId(response.id, AppSetId.SCOPE_DEVELOPER)
+        }
+    }
+
+    companion object {
+
+        /**
+         *  Creates [AppSetIdManager].
+         *
+         *  @return AppSetIdManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): AppSetIdManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                // TODO(b/261770989): Extend this to older versions.
+                null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
new file mode 100644
index 0000000..ec458ba
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdData.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+import android.net.Uri
+
+/**
+ * Represents data specific to an ad that is necessary for ad selection and rendering.
+ * @param renderUri a URI pointing to the ad's rendering assets
+ * @param metadata buyer ad metadata represented as a JSON string
+ */
+class AdData public constructor(
+    val renderUri: Uri,
+    val metadata: String
+    ) {
+
+    /** Checks whether two [AdData] objects contain the same information.  */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdData) return false
+        return this.renderUri == other.renderUri &&
+            this.metadata == other.metadata
+    }
+
+    /** Returns the hash of the [AdData] object's data.  */
+    override fun hashCode(): Int {
+        var hash = renderUri.hashCode()
+        hash = 31 * hash + metadata.hashCode()
+        return hash
+    }
+
+    /** Overrides the toString method.  */
+    override fun toString(): String {
+        return "AdData: renderUri=$renderUri, metadata='$metadata'"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
new file mode 100644
index 0000000..5495ae5
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdSelectionSignals.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+/**
+ * This class holds JSON that will be passed into a JavaScript function during ad selection. Its
+ * contents are not used by <a
+ * href="https://developer.android.com/design-for-safety/privacy-sandbox/fledge">FLEDGE</a> platform
+ * code, but are merely validated and then passed to the appropriate JavaScript ad selection
+ * function.
+ * @param signals Any valid JSON string to create the AdSelectionSignals with.
+ */
+class AdSelectionSignals public constructor(val signals: String) {
+    /**
+     * Compares this AdSelectionSignals to the specified object. The result is true if and only if
+     * the argument is not null and the signals property of the two objects are equal.
+     * Note that this method will not perform any JSON normalization so two AdSelectionSignals
+     * objects with the same JSON could be not equal if the String representations of the objects
+     * was not equal.
+     *
+     * @param other The object to compare this AdSelectionSignals against
+     * @return true if the given object represents an AdSelectionSignals equivalent to this
+     * AdSelectionSignals, false otherwise
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdSelectionSignals) return false
+        return this.signals == other.signals
+    }
+
+    /**
+     * Returns a hash code corresponding to the string representation of this class obtained by
+     * calling [.toString]. Note that this method will not perform any JSON normalization so
+     * two AdSelectionSignals objects with the same JSON could have different hash codes if the
+     * underlying string representation was different.
+     *
+     * @return a hash code value for this object.
+     */
+    override fun hashCode(): Int {
+        return signals.hashCode()
+    }
+
+    /** @return The String form of the JSON wrapped by this class.
+     */
+    override fun toString(): String {
+        return "AdSelectionSignals: $signals"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
new file mode 100644
index 0000000..775e14e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/common/AdTechIdentifier.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.privacysandbox.ads.adservices.common
+
+/**
+ * An Identifier representing an ad buyer or seller.
+ *
+ * @param identifier The identifier.
+ */
+class AdTechIdentifier public constructor(val identifier: String) {
+
+    /**
+     * Compares this AdTechIdentifier to the specified object. The result is true if and only if
+     * the argument is not null and the identifier property of the two objects are equal.
+     * Note that this method will not perform any eTLD+1 normalization so two AdTechIdentifier
+     * objects with the same eTLD+1 could be not equal if the String representations of the objects
+     * was not equal.
+     *
+     * @param other The object to compare this AdTechIdentifier against
+     * @return true if the given object represents an AdTechIdentifier equivalent to this
+     * AdTechIdentifier, false otherwise
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is AdTechIdentifier) return false
+        return this.identifier == other.identifier
+    }
+
+    /**
+     * Returns a hash code corresponding to the string representation of this class obtained by
+     * calling [.toString]. Note that this method will not perform any eTLD+1 normalization
+     * so two AdTechIdentifier objects with the same eTLD+1 could have different hash codes if the
+     * underlying string representation was different.
+     *
+     * @return a hash code value for this object.
+     */
+    override fun hashCode(): Int {
+        return identifier.hashCode()
+    }
+
+    /** @return The identifier in String form.
+     */
+    override fun toString(): String {
+        return "AdTechIdentifier: $identifier"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudience.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudience.kt
new file mode 100644
index 0000000..61e5ff7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudience.kt
@@ -0,0 +1,261 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import java.time.Instant
+
+/**
+ * Represents the information necessary for a custom audience to participate in ad selection.
+ *
+ * A custom audience is an abstract grouping of users with similar demonstrated interests. This
+ * class is a collection of some data stored on a device that is necessary to serve advertisements
+ * targeting a single custom audience.
+ *
+ * @param buyer A buyer is identified by a domain in the form "buyerexample.com".
+ * @param name The custom audience's name is an arbitrary string provided by the owner and buyer on
+ * creation of the [CustomAudience] object.
+ * @param dailyUpdateUri a URI that points to a buyer-operated server that hosts updated bidding
+ * data and ads metadata to be used in the on-device ad selection process. The URI must use HTTPS.
+ * @param biddingLogicUri the target URI used to fetch bidding logic when a custom audience
+ * participates in the ad selection process. The URI must use HTTPS.
+ * @param ads the list of [AdData] objects is a full and complete list of the ads that will be
+ * served by this [CustomAudience] during the ad selection process.
+ * @param activationTime optional activation time may be set in the future, in order to serve a
+ * delayed activation. If the field is not set, the object will be activated at the time of joining.
+ * @param expirationTime optional expiration time. Once it has passed, a custom audience is no
+ * longer eligible for daily ad/bidding data updates or participation in the ad selection process.
+ * The custom audience will then be deleted from memory by the next daily update.
+ * @param userBiddingSignals optional User bidding signals, provided by buyers to be consumed by
+ * buyer-provided JavaScript during ad selection in an isolated execution environment.
+ * @param trustedBiddingSignals optional trusted bidding data, consists of a URI pointing to a
+ * trusted server for buyers' bidding data and a list of keys to query the server with.
+ */
+class CustomAudience public constructor(
+    val buyer: AdTechIdentifier,
+    val name: String,
+    val dailyUpdateUri: Uri,
+    val biddingLogicUri: Uri,
+    val ads: List<AdData>,
+    val activationTime: Instant? = null,
+    val expirationTime: Instant? = null,
+    val userBiddingSignals: AdSelectionSignals? = null,
+    val trustedBiddingSignals: TrustedBiddingData? = null
+) {
+
+    /**
+     * Checks whether two [CustomAudience] objects contain the same information.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is CustomAudience) return false
+        return this.buyer == other.buyer &&
+            this.name == other.name &&
+            this.activationTime == other.activationTime &&
+            this.expirationTime == other.expirationTime &&
+            this.dailyUpdateUri == other.dailyUpdateUri &&
+            this.userBiddingSignals == other.userBiddingSignals &&
+            this.trustedBiddingSignals == other.trustedBiddingSignals &&
+            this.ads == other.ads
+    }
+
+    /**
+     * Returns the hash of the [CustomAudience] object's data.
+     */
+    override fun hashCode(): Int {
+        var hash = buyer.hashCode()
+        hash = 31 * hash + name.hashCode()
+        hash = 31 * hash + activationTime.hashCode()
+        hash = 31 * hash + expirationTime.hashCode()
+        hash = 31 * hash + dailyUpdateUri.hashCode()
+        hash = 31 * hash + userBiddingSignals.hashCode()
+        hash = 31 * hash + trustedBiddingSignals.hashCode()
+        hash = 31 * hash + biddingLogicUri.hashCode()
+        hash = 31 * hash + ads.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "CustomAudience: " +
+            "buyer=$biddingLogicUri, activationTime=$activationTime, " +
+            "expirationTime=$expirationTime, dailyUpdateUri=$dailyUpdateUri, " +
+            "userBiddingSignals=$userBiddingSignals, " +
+            "trustedBiddingSignals=$trustedBiddingSignals, " +
+            "biddingLogicUri=$biddingLogicUri, ads=$ads"
+    }
+
+    /** Builder for [CustomAudience] objects. */
+    @SuppressWarnings("OptionalBuilderConstructorArgument")
+    public class Builder(
+        private var buyer: AdTechIdentifier,
+        private var name: String,
+        private var dailyUpdateUri: Uri,
+        private var biddingLogicUri: Uri,
+        private var ads: List<AdData>
+    ) {
+        private var activationTime: Instant? = null
+        private var expirationTime: Instant? = null
+        private var userBiddingSignals: AdSelectionSignals? = null
+        private var trustedBiddingData: TrustedBiddingData? = null
+
+        /**
+         * Sets the buyer [AdTechIdentifier].
+         *
+         * @param buyer A buyer is identified by a domain in the form "buyerexample.com".
+         */
+        fun setBuyer(buyer: AdTechIdentifier): Builder = apply {
+            this.buyer = buyer
+        }
+
+        /**
+         * Sets the [CustomAudience] object's name.
+         *
+         * @param name  The custom audience's name is an arbitrary string provided by the owner and
+         * buyer on creation of the [CustomAudience] object.
+         */
+        fun setName(name: String): Builder = apply {
+            this.name = name
+        }
+
+        /**
+         * On creation of the [CustomAudience] object, an optional activation time may be set
+         * in the future, in order to serve a delayed activation. If the field is not set, the
+         * [CustomAudience] will be activated at the time of joining.
+         *
+         * For example, a custom audience for lapsed users may not activate until a threshold of
+         * inactivity is reached, at which point the custom audience's ads will participate in the
+         * ad selection process, potentially redirecting lapsed users to the original owner
+         * application.
+         *
+         * The maximum delay in activation is 60 days from initial creation.
+         *
+         * If specified, the activation time must be an earlier instant than the expiration time.
+         *
+         * @param activationTime activation time, truncated to milliseconds, after which the
+         * [CustomAudience] will serve ads.
+         */
+        fun setActivationTime(activationTime: Instant): Builder = apply {
+            this.activationTime = activationTime
+        }
+
+        /**
+         * Once the expiration time has passed, a custom audience is no longer eligible for daily
+         * ad/bidding data updates or participation in the ad selection process. The custom audience
+         * will then be deleted from memory by the next daily update.
+         *
+         * If no expiration time is provided on creation of the [CustomAudience], expiry will
+         * default to 60 days from activation.
+         *
+         * The maximum expiry is 60 days from initial activation.
+         *
+         * @param expirationTime the timestamp [Instant], truncated to milliseconds, after
+         * which the custom audience should be removed.
+         */
+        fun setExpirationTime(expirationTime: Instant): Builder = apply {
+            this.expirationTime = expirationTime
+        }
+
+        /**
+         * This URI points to a buyer-operated server that hosts updated bidding data and ads
+         * metadata to be used in the on-device ad selection process. The URI must use HTTPS.
+         *
+         * @param dailyUpdateUri the custom audience's daily update URI
+         */
+        fun setDailyUpdateUri(dailyUpdateUri: Uri): Builder = apply {
+            this.dailyUpdateUri = dailyUpdateUri
+        }
+
+        /**
+         * User bidding signals are optionally provided by buyers to be consumed by buyer-provided
+         * JavaScript during ad selection in an isolated execution environment.
+         *
+         * If the user bidding signals are not a valid JSON object that can be consumed by the
+         * buyer's JS, the custom audience will not be eligible for ad selection.
+         *
+         * If not specified, the [CustomAudience] will not participate in ad selection
+         * until user bidding signals are provided via the daily update for the custom audience.
+         *
+         * @param userBiddingSignals an [AdSelectionSignals] object representing the user
+         * bidding signals for the custom audience
+         */
+        fun setUserBiddingSignals(userBiddingSignals: AdSelectionSignals): Builder = apply {
+            this.userBiddingSignals = userBiddingSignals
+        }
+
+        /**
+         * Trusted bidding data consists of a URI pointing to a trusted server for buyers' bidding data
+         * and a list of keys to query the server with. Note that the keys are arbitrary identifiers
+         * that will only be used to query the trusted server for a buyer's bidding logic during ad
+         * selection.
+         *
+         * If not specified, the [CustomAudience] will not participate in ad selection
+         * until trusted bidding data are provided via the daily update for the custom audience.
+         *
+         * @param trustedBiddingSignals a [TrustedBiddingData] object containing the custom
+         * audience's trusted bidding data.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        fun setTrustedBiddingData(trustedBiddingSignals: TrustedBiddingData): Builder = apply {
+            this.trustedBiddingData = trustedBiddingSignals
+        }
+
+        /**
+         * Returns the target URI used to fetch bidding logic when a custom audience participates in the
+         * ad selection process. The URI must use HTTPS.
+         *
+         * @param biddingLogicUri the URI for fetching buyer bidding logic
+         */
+        fun setBiddingLogicUri(biddingLogicUri: Uri): Builder = apply {
+            this.biddingLogicUri = biddingLogicUri
+        }
+
+        /**
+         * This list of [AdData] objects is a full and complete list of the ads that will be
+         * served by this [CustomAudience] during the ad selection process.
+         *
+         * If not specified, or if an empty list is provided, the [CustomAudience] will not
+         * participate in ad selection until a valid list of ads are provided via the daily update
+         * for the custom audience.
+         *
+         * @param ads a [List] of [AdData] objects representing ads currently served by
+         * the custom audience.
+         */
+        fun setAds(ads: List<AdData>): Builder = apply {
+            this.ads = ads
+        }
+
+        /**
+         * Builds an instance of a [CustomAudience].
+         */
+        fun build(): CustomAudience {
+            return CustomAudience(
+                buyer,
+                name,
+                dailyUpdateUri,
+                biddingLogicUri,
+                ads,
+                activationTime,
+                expirationTime,
+                userBiddingSignals,
+                trustedBiddingData
+            )
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
new file mode 100644
index 0000000..d78c779
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/CustomAudienceManager.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * This class provides APIs for app and ad-SDKs to join / leave custom audiences.
+ */
+abstract class CustomAudienceManager internal constructor() {
+    /**
+     * Adds the user to the given [CustomAudience].
+     *
+     * An attempt to register the user for a custom audience with the same combination of {@code
+     * ownerPackageName}, {@code buyer}, and {@code name} will cause the existing custom audience's
+     * information to be overwritten, including the list of ads data.
+     *
+     * Note that the ads list can be completely overwritten by the daily background fetch job.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with an [IllegalArgumentException] if
+     *
+     * <ol>
+     *   <li>the storage limit has been exceeded by the calling application and/or
+     *   <li>any URI parameters in the [CustomAudience] given are not authenticated with the
+     *       [CustomAudience] buyer.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call fails with an [IllegalStateException] if an internal service error is
+     * encountered.
+     *
+     * @param request The request to join custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun joinCustomAudience(request: JoinCustomAudienceRequest)
+
+    /**
+     * Attempts to remove a user from a custom audience by deleting any existing [CustomAudience]
+     * data, identified by {@code ownerPackageName}, {@code buyer}, and {@code
+     * name}.
+     *
+     * This call fails with an [SecurityException] if
+     *
+     * <ol>
+     *   <li>the {@code ownerPackageName} is not calling app's package name; and/or
+     *   <li>the buyer is not authorized to use the API.
+     * </ol>
+     *
+     * This call fails with [LimitExceededException] if the calling package exceeds the
+     * allowed rate limits and is throttled.
+     *
+     * This call does not inform the caller whether the custom audience specified existed in
+     * on-device storage. In other words, it will fail silently when a buyer attempts to leave a
+     * custom audience that was not joined.
+     *
+     * @param request The request to leave custom audience.
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+    abstract suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest)
+
+    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val customAudienceManager: android.adservices.customaudience.CustomAudienceManager
+        ) : CustomAudienceManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.customaudience.CustomAudienceManager>(
+                android.adservices.customaudience.CustomAudienceManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun joinCustomAudience(request: JoinCustomAudienceRequest) {
+            suspendCancellableCoroutine { continuation ->
+                customAudienceManager.joinCustomAudience(
+                    convertJoinRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE)
+        override suspend fun leaveCustomAudience(request: LeaveCustomAudienceRequest) {
+            suspendCancellableCoroutine { continuation ->
+                customAudienceManager.leaveCustomAudience(
+                    convertLeaveRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        private fun convertJoinRequest(
+            request: JoinCustomAudienceRequest
+        ): android.adservices.customaudience.JoinCustomAudienceRequest {
+            return android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                .setCustomAudience(convertCustomAudience(request.customAudience))
+                .build()
+        }
+
+        private fun convertLeaveRequest(
+            request: LeaveCustomAudienceRequest
+        ): android.adservices.customaudience.LeaveCustomAudienceRequest {
+            return android.adservices.customaudience.LeaveCustomAudienceRequest.Builder()
+                .setBuyer(convertAdTechIdentifier(request.buyer))
+                .setName(request.name)
+                .build()
+        }
+
+        private fun convertCustomAudience(
+            request: CustomAudience
+        ): android.adservices.customaudience.CustomAudience {
+            return android.adservices.customaudience.CustomAudience.Builder()
+                .setActivationTime(request.activationTime)
+                .setAds(convertAdData(request.ads))
+                .setBiddingLogicUri(request.biddingLogicUri)
+                .setBuyer(convertAdTechIdentifier(request.buyer))
+                .setDailyUpdateUri(request.dailyUpdateUri)
+                .setExpirationTime(request.expirationTime)
+                .setName(request.name)
+                .setTrustedBiddingData(convertTrustedSignals(request.trustedBiddingSignals))
+                .setUserBiddingSignals(convertBiddingSignals(request.userBiddingSignals))
+                .build()
+        }
+
+        private fun convertAdData(
+            input: List<AdData>
+        ): List<android.adservices.common.AdData> {
+            val result = mutableListOf<android.adservices.common.AdData>()
+            for (ad in input) {
+                result.add(android.adservices.common.AdData.Builder()
+                    .setMetadata(ad.metadata)
+                    .setRenderUri(ad.renderUri)
+                    .build())
+            }
+            return result
+        }
+
+        private fun convertAdTechIdentifier(
+            input: AdTechIdentifier
+        ): android.adservices.common.AdTechIdentifier {
+            return android.adservices.common.AdTechIdentifier.fromString(input.identifier)
+        }
+
+        private fun convertTrustedSignals(
+            input: TrustedBiddingData?
+        ): android.adservices.customaudience.TrustedBiddingData? {
+            if (input == null) return null
+            return android.adservices.customaudience.TrustedBiddingData.Builder()
+                .setTrustedBiddingKeys(input.trustedBiddingKeys)
+                .setTrustedBiddingUri(input.trustedBiddingUri)
+                .build()
+        }
+
+        private fun convertBiddingSignals(
+            input: AdSelectionSignals?
+        ): android.adservices.common.AdSelectionSignals? {
+            if (input == null) return null
+            return android.adservices.common.AdSelectionSignals.fromString(input.signals)
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [CustomAudienceManager].
+         *
+         *  @return CustomAudienceManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): CustomAudienceManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequest.kt
new file mode 100644
index 0000000..11d5703
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/JoinCustomAudienceRequest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+/**
+ * The request object to join a custom audience.
+ *
+ * @param customAudience the custom audience to join.
+ */
+class JoinCustomAudienceRequest public constructor(val customAudience: CustomAudience) {
+    /**
+     * Checks whether two [JoinCustomAudienceRequest] objects contain the same information.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is JoinCustomAudienceRequest) return false
+        return this.customAudience == other.customAudience
+    }
+
+    /**
+     * Returns the hash of the [JoinCustomAudienceRequest] object's data.
+     */
+    override fun hashCode(): Int {
+        return customAudience.hashCode()
+    }
+
+    override fun toString(): String {
+        return "JoinCustomAudience: customAudience=$customAudience"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceRequest.kt
new file mode 100644
index 0000000..ca60ccf
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/LeaveCustomAudienceRequest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+
+/**
+ * The request object to leave a custom audience.
+ *
+ * @param buyer an [AdTechIdentifier] containing the custom audience's buyer's domain.
+ * @param name the String name of the custom audience.
+ */
+class LeaveCustomAudienceRequest public constructor(
+    val buyer: AdTechIdentifier,
+    val name: String
+    ) {
+
+    /**
+     * Checks whether two [LeaveCustomAudienceRequest] objects contain the same information.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is LeaveCustomAudienceRequest) return false
+        return this.buyer == other.buyer && this.name == other.name
+    }
+
+    /**
+     * Returns the hash of the [LeaveCustomAudienceRequest] object's data.
+     */
+    override fun hashCode(): Int {
+        return (31 * buyer.hashCode()) + name.hashCode()
+    }
+
+    override fun toString(): String {
+        return "LeaveCustomAudience: buyer=$buyer, name=$name"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
new file mode 100644
index 0000000..fef0a18
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/customaudience/TrustedBiddingData.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.privacysandbox.ads.adservices.customaudience
+
+import android.net.Uri
+
+/**
+ * Represents data used during the ad selection process to fetch buyer bidding signals from a
+ * trusted key/value server. The fetched data is used during the ad selection process and consumed
+ * by buyer JavaScript logic running in an isolated execution environment.
+ *
+ * @param trustedBiddingUri the URI pointing to the trusted key-value server holding bidding
+ * signals. The URI must use HTTPS.
+ * @param trustedBiddingKeys the list of keys to query from the trusted key-value server holding
+ * bidding signals.
+ */
+class TrustedBiddingData public constructor(
+    val trustedBiddingUri: Uri,
+    val trustedBiddingKeys: List<String>
+    ) {
+    /**
+     * @return `true` if two [TrustedBiddingData] objects contain the same information
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TrustedBiddingData) return false
+        return this.trustedBiddingUri == other.trustedBiddingUri &&
+            this.trustedBiddingKeys == other.trustedBiddingKeys
+    }
+
+    /**
+     * @return the hash of the [TrustedBiddingData] object's data
+     */
+    override fun hashCode(): Int {
+        return (31 * trustedBiddingUri.hashCode()) + trustedBiddingKeys.hashCode()
+    }
+
+    override fun toString(): String {
+        return "TrustedBiddingData: trustedBiddingUri=$trustedBiddingUri " +
+            "trustedBiddingKeys=$trustedBiddingKeys"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
new file mode 100644
index 0000000..5f8544d
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/AdServicesInfo.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.privacysandbox.ads.adservices.internal
+
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+/**
+ * Temporary replacement for BuildCompat.AD_SERVICES_EXTENSION_INT.
+ * TODO(b/261755947) Replace with AD_SERVICES_EXTENSION_INT after new core library release
+ *
+ * @suppress
+ */
+internal object AdServicesInfo {
+
+    fun version(): Int {
+        return if (Build.VERSION.SDK_INT >= 30) {
+            Extensions30Impl.getAdServicesVersion()
+        } else {
+            0
+        }
+    }
+
+    @RequiresApi(30)
+    private object Extensions30Impl {
+        @DoNotInline
+        fun getAdServicesVersion() =
+            SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/package-info.java
similarity index 79%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/package-info.java
index 7053e2d..5e13308 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/internal/package-info.java
@@ -14,6 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.privacysandbox.ads.adservices.internal;
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
new file mode 100644
index 0000000..1af0d2f
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/DeletionRequest.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.time.Instant
+
+/**
+ * Deletion Request.
+ * @param deletionMode Set the deletion mode for the supplied params.
+ *     [DELETION_MODE_ALL]: All data associated with the selected records will be
+ *     deleted.
+ *     [DELETION_MODE_EXCLUDE_INTERNAL_DATA]: All data except the internal system
+ *     data (e.g. rate limits) associated with the selected records will be deleted.
+ *
+ * @param matchBehavior Set the match behavior for the supplied params.
+ *     [MATCH_BEHAVIOR_DELETE]: This option will use the supplied params
+ *     (Origin URIs & Domain URIs) for selecting records for deletion.
+ *     [MATCH_BEHAVIOR_PRESERVE]: This option will preserve the data associated with the
+ *     supplied params (Origin URIs & Domain URIs) and select remaining records for deletion.
+ *
+ * @param start [Instant] Set the start of the deletion range. Not setting this or
+ *     passing in [java.time.Instant#MIN] will cause everything from the oldest record to
+ *     the specified end be deleted.
+ *
+ * @param end [Instant] Set the end of the deletion range. Not setting this or passing in
+ *     [java.time.Instant#MAX] will cause everything from the specified start until the
+ *     newest record to be deleted.
+ *
+ * @param domainUris the list of domain URI which will be used for matching. These will be matched
+ *     with records using the same domain or any subdomains. E.g. If domainUri is {@code
+ *     https://example.com}, then {@code https://a.example.com}, {@code https://example.com} and
+ *     {@code https://b.example.com} will match; {@code https://abcexample.com} will NOT match.
+ *     A null or empty list will match everything.
+ *
+ * @param originUris the list of origin URI which will be used for matching. These will be matched
+ *     with records using the same origin only, i.e. subdomains won't match. E.g. If originUri is
+ *     {@code https://a.example.com}, then {@code https://a.example.com} will match; {@code
+ *     https://example.com}, {@code https://b.example.com} and {@code https://abcexample.com}
+ *     will NOT match. A null or empty list will match everything.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class DeletionRequest(
+    val deletionMode: Int,
+    val matchBehavior: Int,
+    val start: Instant = Instant.MIN,
+    val end: Instant = Instant.MAX,
+    val domainUris: List<Uri> = emptyList(),
+    val originUris: List<Uri> = emptyList(),
+) {
+
+    init {
+        require(deletionMode == DELETION_MODE_ALL ||
+            deletionMode == DELETION_MODE_EXCLUDE_INTERNAL_DATA) {
+            "DeletionMode undefined."
+        }
+
+        require(matchBehavior == MATCH_BEHAVIOR_DELETE ||
+            matchBehavior == MATCH_BEHAVIOR_PRESERVE) {
+            "MatchBehavior undefined."
+        }
+    }
+
+    override fun hashCode(): Int {
+        var hash = deletionMode.hashCode()
+        hash = 31 * hash + domainUris.hashCode()
+        hash = 31 * hash + originUris.hashCode()
+        hash = 31 * hash + start.hashCode()
+        hash = 31 * hash + end.hashCode()
+        hash = 31 * hash + matchBehavior.hashCode()
+        return hash
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DeletionRequest) return false
+        return this.deletionMode == other.deletionMode &&
+            HashSet(this.domainUris) == HashSet(other.domainUris) &&
+            HashSet(this.originUris) == HashSet(other.originUris) &&
+            this.start == other.start &&
+            this.end == other.end &&
+            this.matchBehavior == other.matchBehavior
+    }
+
+    override fun toString(): String {
+        val deletionModeStr = if (deletionMode == DELETION_MODE_ALL) "DELETION_MODE_ALL"
+        else "DELETION_MODE_EXCLUDE_INTERNAL_DATA"
+        val matchBehaviorStr = if (matchBehavior == MATCH_BEHAVIOR_DELETE) "MATCH_BEHAVIOR_DELETE"
+        else "MATCH_BEHAVIOR_PRESERVE"
+        return "DeletionRequest { DeletionMode=$deletionModeStr, " +
+            "MatchBehavior=$matchBehaviorStr, " +
+            "Start=$start, End=$end, DomainUris=$domainUris, OriginUris=$originUris }"
+    }
+
+    companion object {
+        /** Deletion mode to delete all data associated with the selected records.  */
+        public const val DELETION_MODE_ALL = 0
+
+        /**
+         * Deletion mode to delete all data except the internal data (e.g. rate limits) for the
+         * selected records.
+         */
+        public const val DELETION_MODE_EXCLUDE_INTERNAL_DATA = 1
+
+        /** Match behavior option to delete the supplied params (Origin/Domains).  */
+        public const val MATCH_BEHAVIOR_DELETE = 0
+
+        /**
+         * Match behavior option to preserve the supplied params (Origin/Domains) and delete
+         * everything else.
+         */
+        public const val MATCH_BEHAVIOR_PRESERVE = 1
+    }
+
+    /**
+     * Builder for {@link DeletionRequest} objects.
+     *
+     * @param deletionMode {@link DeletionMode} Set the match behavior for the supplied params.
+     *     {@link #DELETION_MODE_ALL}: All data associated with the selected records will be
+     *     deleted.
+     *     {@link #DELETION_MODE_EXCLUDE_INTERNAL_DATA}: All data except the internal system
+     *     data (e.g. rate limits) associated with the selected records will be deleted.
+     *
+     * @param matchBehavior {@link MatchBehavior} Set the match behavior for the supplied params.
+     *     {@link #MATCH_BEHAVIOR_DELETE}: This option will use the supplied params
+     *     (Origin URIs & Domain URIs) for selecting records for deletion.
+     *     {@link #MATCH_BEHAVIOR_PRESERVE}: This option will preserve the data associated with the
+     *     supplied params (Origin URIs & Domain URIs) and select remaining records for deletion.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public class Builder constructor(
+        private val deletionMode: Int,
+        private val matchBehavior: Int
+    ) {
+        private var start: Instant = Instant.MIN
+        private var end: Instant = Instant.MAX
+        private var domainUris: List<Uri> = emptyList()
+        private var originUris: List<Uri> = emptyList()
+
+        /**
+         * Sets the start of the deletion range. Not setting this or passing in
+         * {@link java.time.Instant#MIN} will cause everything from the oldest record to the
+         * specified end be deleted.
+         */
+        fun setStart(start: Instant): Builder = apply {
+            this.start = start
+        }
+
+        /**
+         * Sets the end of the deletion range. Not setting this or passing in
+         * {@link java.time.Instant#MAX} will cause everything from the specified start until the
+         * newest record to be deleted.
+         */
+        fun setEnd(end: Instant): Builder = apply {
+            this.end = end
+        }
+
+        /**
+         * Set the list of domain URI which will be used for matching. These will be matched with
+         * records using the same domain or any subdomains. E.g. If domainUri is {@code
+         * https://example.com}, then {@code https://a.example.com}, {@code https://example.com} and
+         * {@code https://b.example.com} will match; {@code https://abcexample.com} will NOT match.
+         * A null or empty list will match everything.
+         */
+        fun setDomainUris(domainUris: List<Uri>): Builder = apply {
+            this.domainUris = domainUris
+        }
+
+        /**
+         * Set the list of origin URI which will be used for matching. These will be matched with
+         * records using the same origin only, i.e. subdomains won't match. E.g. If originUri is
+         * {@code https://a.example.com}, then {@code https://a.example.com} will match; {@code
+         * https://example.com}, {@code https://b.example.com} and {@code https://abcexample.com}
+         * will NOT match. A null or empty list will match everything.
+         */
+        fun setOriginUris(originUris: List<Uri>): Builder = apply {
+            this.originUris = originUris
+        }
+
+        /** Builds a {@link DeletionRequest} instance. */
+        fun build(): DeletionRequest {
+            return DeletionRequest(
+                deletionMode,
+                matchBehavior,
+                start,
+                end,
+                domainUris,
+                originUris)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
new file mode 100644
index 0000000..3309244
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/MeasurementManager.kt
@@ -0,0 +1,259 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.os.ext.SdkExtensions
+import android.view.InputEvent
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * This class provides APIs to manage ads attribution using Privacy Sandbox.
+ */
+abstract class MeasurementManager {
+    /**
+     * Delete previous registrations.
+     *
+     * @param deletionRequest The request for deleting data.
+     */
+    abstract suspend fun deleteRegistrations(deletionRequest: DeletionRequest)
+
+    /**
+     * Register an attribution source (click or view).
+     *
+     * @param attributionSource the platform issues a request to this URI in order to fetch metadata
+     *     associated with the attribution source.
+     * @param inputEvent either an [InputEvent] object (for a click event) or null (for a view
+     *     event).
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?)
+
+    /**
+     * Register a trigger (conversion).
+     *
+     * @param trigger the API issues a request to this URI to fetch metadata associated with the
+     *     trigger.
+     */
+    // TODO(b/258551492): Improve docs.
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerTrigger(trigger: Uri)
+
+    /**
+     * Register an attribution source(click or view) from web context. This API will not process any
+     * redirects, all registration URLs should be supplied with the request. At least one of
+     * appDestination or webDestination parameters are required to be provided.
+     *
+     * @param request source registration request
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerWebSource(request: WebSourceRegistrationRequest)
+
+    /**
+     * Register an attribution trigger(click or view) from web context. This API will not process
+     * any redirects, all registration URLs should be supplied with the request.
+     *
+     * @param request trigger registration request
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest)
+
+    /**
+     * Get Measurement API status.
+     *
+     * The call returns an integer value (see [MEASUREMENT_API_STATE_DISABLED] and
+     * [MEASUREMENT_API_STATE_ENABLED] for possible values).
+     */
+    @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+    abstract suspend fun getMeasurementApiStatus(): Int
+
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mMeasurementManager: android.adservices.measurement.MeasurementManager
+    ) : MeasurementManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.measurement.MeasurementManager>(
+                android.adservices.measurement.MeasurementManager::class.java
+            )
+        )
+
+        @DoNotInline
+        override suspend fun deleteRegistrations(deletionRequest: DeletionRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.deleteRegistrations(
+                    convertDeletionRequest(deletionRequest),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        private fun convertDeletionRequest(
+            request: DeletionRequest
+        ): android.adservices.measurement.DeletionRequest {
+            return android.adservices.measurement.DeletionRequest.Builder()
+                .setDeletionMode(request.deletionMode)
+                .setMatchBehavior(request.matchBehavior)
+                .setStart(request.start)
+                .setEnd(request.end)
+                .setDomainUris(request.domainUris)
+                .setOriginUris(request.originUris)
+                .build()
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerSource(attributionSource: Uri, inputEvent: InputEvent?) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerSource(
+                    attributionSource,
+                    inputEvent,
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerTrigger(trigger: Uri) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerTrigger(
+                    trigger,
+                    Runnable::run,
+                    continuation.asOutcomeReceiver())
+            }
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerWebSource(request: WebSourceRegistrationRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerWebSource(
+                    convertWebSourceRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver())
+            }
+        }
+
+        private fun convertWebSourceRequest(
+            request: WebSourceRegistrationRequest
+        ): android.adservices.measurement.WebSourceRegistrationRequest {
+            return android.adservices.measurement.WebSourceRegistrationRequest
+                .Builder(
+                    convertWebSourceParams(request.webSourceParams),
+                    request.topOriginUri)
+                .setWebDestination(request.webDestination)
+                .setAppDestination(request.appDestination)
+                .setInputEvent(request.inputEvent)
+                .setVerifiedDestination(request.verifiedDestination)
+                .build()
+        }
+
+        private fun convertWebSourceParams(
+            request: List<WebSourceParams>
+        ): List<android.adservices.measurement.WebSourceParams> {
+            var result = mutableListOf<android.adservices.measurement.WebSourceParams>()
+            for (param in request) {
+                result.add(android.adservices.measurement.WebSourceParams
+                    .Builder(param.registrationUri)
+                    .setDebugKeyAllowed(param.debugKeyAllowed)
+                    .build())
+            }
+            return result
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun registerWebTrigger(request: WebTriggerRegistrationRequest) {
+            suspendCancellableCoroutine<Any> { continuation ->
+                mMeasurementManager.registerWebTrigger(
+                    convertWebTriggerRequest(request),
+                    Runnable::run,
+                    continuation.asOutcomeReceiver())
+            }
+        }
+
+        private fun convertWebTriggerRequest(
+            request: WebTriggerRegistrationRequest
+        ): android.adservices.measurement.WebTriggerRegistrationRequest {
+            return android.adservices.measurement.WebTriggerRegistrationRequest
+                .Builder(
+                    convertWebTriggerParams(request.webTriggerParams),
+                    request.destination)
+                .build()
+        }
+
+        private fun convertWebTriggerParams(
+            request: List<WebTriggerParams>
+        ): List<android.adservices.measurement.WebTriggerParams> {
+            var result = mutableListOf<android.adservices.measurement.WebTriggerParams>()
+            for (param in request) {
+                result.add(android.adservices.measurement.WebTriggerParams
+                    .Builder(param.registrationUri)
+                    .setDebugKeyAllowed(param.debugKeyAllowed)
+                    .build())
+            }
+            return result
+        }
+
+        @DoNotInline
+        @RequiresPermission(ACCESS_ADSERVICES_ATTRIBUTION)
+        override suspend fun getMeasurementApiStatus(): Int = suspendCancellableCoroutine {
+                continuation ->
+            mMeasurementManager.getMeasurementApiStatus(
+                Runnable::run,
+                continuation.asOutcomeReceiver())
+        }
+    }
+
+    companion object {
+        /**
+         * This state indicates that Measurement APIs are unavailable. Invoking them will result
+         * in an [UnsupportedOperationException].
+         */
+        public const val MEASUREMENT_API_STATE_DISABLED = 0
+        /**
+         * This state indicates that Measurement APIs are enabled.
+         */
+        public const val MEASUREMENT_API_STATE_ENABLED = 1
+
+        /**
+         *  Creates [MeasurementManager].
+         *
+         *  @return MeasurementManager object.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): MeasurementManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
new file mode 100644
index 0000000..b37465a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceParams.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * Class holding source registration parameters.
+ *
+ * @param registrationUri URI that the Attribution Reporting API sends a request to in order to
+ * obtain source registration parameters.
+ * @param debugKeyAllowed Used by the browser to indicate whether the debug key obtained from the
+ * registration URI is allowed to be used.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebSourceParams public constructor(
+    val registrationUri: Uri,
+    val debugKeyAllowed: Boolean
+    ) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebSourceParams) return false
+        return this.registrationUri == other.registrationUri &&
+            this.debugKeyAllowed == other.debugKeyAllowed
+    }
+
+    override fun hashCode(): Int {
+        var hash = registrationUri.hashCode()
+        hash = 31 * hash + debugKeyAllowed.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "WebSourceParams { RegistrationUri=$registrationUri, " +
+            "DebugKeyAllowed=$debugKeyAllowed }"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
new file mode 100644
index 0000000..c85be19
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebSourceRegistrationRequest.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import android.view.InputEvent
+import androidx.annotation.RequiresApi
+
+/**
+ * Class to hold input to measurement source registration calls from web context.
+ *
+ * @param webSourceParams Registration info to fetch sources.
+ * @param topOriginUri Top level origin of publisher.
+ * @param inputEvent User Interaction {@link InputEvent} used by the AttributionReporting API to
+ * distinguish clicks from views.
+ * @param appDestination App destination of the source. It is the android app {@link Uri} where
+ * corresponding conversion is expected. At least one of app destination or web destination is
+ * required.
+ * @param webDestination Web destination of the source. It is the website {@link Uri} where
+ * corresponding conversion is expected. At least one of app destination or web destination is
+ * required.
+ * @param verifiedDestination Verified destination by the caller. This is where the user actually
+ * landed.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebSourceRegistrationRequest public constructor(
+    val webSourceParams: List<WebSourceParams>,
+    val topOriginUri: Uri,
+    val inputEvent: InputEvent? = null,
+    val appDestination: Uri? = null,
+    val webDestination: Uri? = null,
+    val verifiedDestination: Uri? = null
+    ) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebSourceRegistrationRequest) return false
+        return this.webSourceParams == other.webSourceParams &&
+            this.webDestination == other.webDestination &&
+            this.appDestination == other.appDestination &&
+            this.topOriginUri == other.topOriginUri &&
+            this.inputEvent == other.inputEvent &&
+            this.verifiedDestination == other.verifiedDestination
+    }
+
+    override fun hashCode(): Int {
+        var hash = webSourceParams.hashCode()
+        hash = 31 * hash + topOriginUri.hashCode()
+        if (inputEvent != null) {
+            hash = 31 * hash + inputEvent.hashCode()
+        }
+        if (appDestination != null) {
+            hash = 31 * hash + appDestination.hashCode()
+        }
+        if (webDestination != null) {
+            hash = 31 * hash + webDestination.hashCode()
+        }
+        // Since topOriginUri is non-null.
+        hash = 31 * hash + topOriginUri.hashCode()
+        if (inputEvent != null) {
+            hash = 31 * hash + inputEvent.hashCode()
+        }
+        if (verifiedDestination != null) {
+            hash = 31 * hash + verifiedDestination.hashCode()
+        }
+        return hash
+    }
+
+    override fun toString(): String {
+        val vals = "WebSourceParams=[$webSourceParams], TopOriginUri=$topOriginUri, " +
+            "InputEvent=$inputEvent, AppDestination=$appDestination, " +
+            "WebDestination=$webDestination, VerifiedDestination=$verifiedDestination"
+        return "WebSourceRegistrationRequest { $vals }"
+    }
+
+    /**
+     * Builder for [WebSourceRegistrationRequest].
+     *
+     * @param webSourceParams source parameters containing source registration parameters, the
+     *     list should not be empty
+     * @param topOriginUri source publisher [Uri]
+     */
+    public class Builder(
+        private val webSourceParams: List<WebSourceParams>,
+        private val topOriginUri: Uri
+    ) {
+        private var inputEvent: InputEvent? = null
+        private var appDestination: Uri? = null
+        private var webDestination: Uri? = null
+        private var verifiedDestination: Uri? = null
+
+        /**
+         * Setter for input event.
+         *
+         * @param inputEvent User Interaction InputEvent used by the AttributionReporting API to
+         *     distinguish clicks from views.
+         * @return builder
+         */
+        fun setInputEvent(inputEvent: InputEvent): Builder = apply {
+            this.inputEvent = inputEvent
+        }
+
+        /**
+         * Setter for app destination. It is the android app {@link Uri} where corresponding
+         * conversion is expected. At least one of app destination or web destination is required.
+         *
+         * @param appDestination app destination [Uri]
+         * @return builder
+         */
+        fun setAppDestination(appDestination: Uri?): Builder = apply {
+            this.appDestination = appDestination
+        }
+
+        /**
+         * Setter for web destination. It is the website {@link Uri} where corresponding conversion
+         * is expected. At least one of app destination or web destination is required.
+         *
+         * @param webDestination web destination [Uri]
+         * @return builder
+         */
+        fun setWebDestination(webDestination: Uri?): Builder = apply {
+            this.webDestination = webDestination
+        }
+
+        /**
+         * Setter for verified destination.
+         *
+         * @param verifiedDestination verified destination
+         * @return builder
+         */
+        fun setVerifiedDestination(verifiedDestination: Uri?): Builder = apply {
+            this.verifiedDestination = verifiedDestination
+        }
+
+        /** Pre-validates parameters and builds [WebSourceRegistrationRequest]. */
+        fun build(): WebSourceRegistrationRequest {
+            return WebSourceRegistrationRequest(
+                webSourceParams,
+                topOriginUri,
+                inputEvent,
+                appDestination,
+                webDestination,
+                verifiedDestination
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
new file mode 100644
index 0000000..ec91bda
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerParams.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * Class holding trigger registration parameters.
+ *
+ * @param registrationUri URI that the Attribution Reporting API sends a request to in order to
+ * obtain trigger registration parameters.
+ * @param debugKeyAllowed Used by the browser to indicate whether the debug key obtained from the
+ * registration URI is allowed to be used.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebTriggerParams public constructor(
+    val registrationUri: Uri,
+    val debugKeyAllowed: Boolean
+    ) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebTriggerParams) return false
+        return this.registrationUri == other.registrationUri &&
+            this.debugKeyAllowed == other.debugKeyAllowed
+    }
+
+    override fun hashCode(): Int {
+        var hash = registrationUri.hashCode()
+        hash = 31 * hash + debugKeyAllowed.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "WebTriggerParams { RegistrationUri=$registrationUri, " +
+            "DebugKeyAllowed=$debugKeyAllowed }"
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
new file mode 100644
index 0000000..6cbd612
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/measurement/WebTriggerRegistrationRequest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.privacysandbox.ads.adservices.measurement
+
+import android.net.Uri
+import android.os.Build
+import androidx.annotation.RequiresApi
+
+/**
+ * Class to hold input to measurement trigger registration calls from web context.
+ *
+ * @param webTriggerParams Registration info to fetch sources.
+ * @param destination Destination [Uri].
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WebTriggerRegistrationRequest public constructor(
+    val webTriggerParams: List<WebTriggerParams>,
+    val destination: Uri
+    ) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is WebTriggerRegistrationRequest) return false
+        return this.webTriggerParams == other.webTriggerParams &&
+            this.destination == other.destination
+    }
+
+    override fun hashCode(): Int {
+        var hash = webTriggerParams.hashCode()
+        hash = 31 * hash + destination.hashCode()
+        return hash
+    }
+
+    override fun toString(): String {
+        return "WebTriggerRegistrationRequest { WebTriggerParams=$webTriggerParams, " +
+            "Destination=$destination"
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/package-info.java
similarity index 77%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/package-info.java
index 7053e2d..2a6c0d8 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
-
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+/**
+ * Privacy Preserving APIs for Privacy Sandbox.
+ */
+package androidx.privacysandbox.ads.adservices;
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
new file mode 100644
index 0000000..1bfa0d37
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsRequest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+/**
+ * Represents the request for the getTopics API (which takes a [GetTopicsRequest] and
+ * returns a [GetTopicsResponse].
+ *
+ * @param sdkName The Ads SDK name. This must be called by SDKs running outside of the Sandbox.
+ * Other clients must not call it.
+ * @param shouldRecordObservation whether to record that the caller has observed the topics of the
+ *     host app or not. This will be used to determine if the caller can receive the topic
+ *     in the next epoch.
+ */
+class GetTopicsRequest public constructor(
+    val sdkName: String = "",
+    @get:JvmName("shouldRecordObservation")
+    val shouldRecordObservation: Boolean = false
+) {
+    override fun toString(): String {
+        return "GetTopicsRequest: " +
+            "sdkName=$sdkName, shouldRecordObservation=$shouldRecordObservation"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is GetTopicsRequest) return false
+        return this.sdkName == other.sdkName &&
+            this.shouldRecordObservation == other.shouldRecordObservation
+    }
+
+    override fun hashCode(): Int {
+        var hash = sdkName.hashCode()
+        hash = 31 * hash + shouldRecordObservation.hashCode()
+        return hash
+    }
+
+    /**
+     * Builder for [GetTopicsRequest].
+     */
+    public class Builder() {
+        private var sdkName: String = ""
+        private var shouldRecordObservation: Boolean = true
+
+        /**
+         * Set Ads Sdk Name.
+         *
+         * <p>This must be called by SDKs running outside of the Sandbox. Other clients must not
+         * call it.
+         *
+         * @param sdkName the Ads Sdk Name.
+         */
+        fun setSdkName(sdkName: String): Builder = apply { this.sdkName = sdkName }
+
+        /**
+         * Set the Record Observation.
+         *
+         * @param shouldRecordObservation whether to record that the caller has observed the topics of the
+         *     host app or not. This will be used to determine if the caller can receive the topic
+         *     in the next epoch.
+         */
+        @Suppress("MissingGetterMatchingBuilder")
+        fun setShouldRecordObservation(shouldRecordObservation: Boolean): Builder = apply {
+            this.shouldRecordObservation = shouldRecordObservation
+        }
+
+        /** Builds a [GetTopicsRequest] instance. */
+        fun build(): GetTopicsRequest {
+            check(sdkName.isNotEmpty()) { "sdkName must be set" }
+            return GetTopicsRequest(sdkName, shouldRecordObservation)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponse.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponse.kt
new file mode 100644
index 0000000..78e871b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/GetTopicsResponse.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import java.util.Objects
+
+/** Represent the result from the getTopics API. */
+class GetTopicsResponse(val topics: List<Topic>) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is GetTopicsResponse) return false
+        if (topics.size != other.topics.size) return false
+        return HashSet(this.topics) == HashSet(other.topics)
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(topics)
+    }
+
+    override fun toString(): String {
+        return "Topics=$topics"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/Topic.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/Topic.kt
new file mode 100644
index 0000000..69ab3ec
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/Topic.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+/**
+ * Represent the topic result from the getTopics API.
+ *
+ * @param taxonomyVersion the version of the taxonomy.
+ * @param modelVersion the version of the model.
+ * @param topicId the unique id of a topic.
+ * See https://developer.android.com/design-for-safety/privacy-sandbox/guides/topics for details.
+ */
+class Topic public constructor(
+    val taxonomyVersion: Long,
+    val modelVersion: Long,
+    val topicId: Int
+) {
+    override fun toString(): String {
+        val taxonomyVersionString = "TaxonomyVersion=$taxonomyVersion" +
+            ", ModelVersion=$modelVersion" +
+            ", TopicCode=$topicId }"
+        return "Topic { $taxonomyVersionString"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is Topic) return false
+        return this.taxonomyVersion == other.taxonomyVersion &&
+            this.modelVersion == other.modelVersion &&
+            this.topicId == other.topicId
+    }
+
+    override fun hashCode(): Int {
+        var hash = taxonomyVersion.hashCode()
+        hash = 31 * hash + modelVersion.hashCode()
+        hash = 31 * hash + topicId.hashCode()
+        return hash
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
new file mode 100644
index 0000000..67d7764
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/src/main/java/androidx/privacysandbox/ads/adservices/topics/TopicsManager.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.privacysandbox.ads.adservices.topics
+
+import android.adservices.common.AdServicesPermissions
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.LimitExceededException
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RequiresPermission
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * TopicsManager provides APIs for App and Ad-Sdks to get the user interest topics in a privacy
+ * preserving way.
+ */
+abstract class TopicsManager internal constructor() {
+    /**
+     * Return the topics.
+     *
+     * @param request The GetTopicsRequest for obtaining Topics.
+     * @throws SecurityException if caller is not authorized to call this API.
+     * @throws IllegalStateException if this API is not available.
+     * @throws LimitExceededException if rate limit was reached.
+     * @return GetTopicsResponse
+     */
+    @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+    abstract suspend fun getTopics(request: GetTopicsRequest): GetTopicsResponse
+
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    private class Api33Ext4Impl(
+        private val mTopicsManager: android.adservices.topics.TopicsManager
+        ) : TopicsManager() {
+        constructor(context: Context) : this(
+            context.getSystemService<android.adservices.topics.TopicsManager>(
+                android.adservices.topics.TopicsManager::class.java
+            )
+        )
+
+        @DoNotInline
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+        override suspend fun getTopics(request: GetTopicsRequest): GetTopicsResponse {
+            return convertResponse(getTopicsAsyncInternal(convertRequest(request)))
+        }
+
+        @RequiresPermission(AdServicesPermissions.ACCESS_ADSERVICES_TOPICS)
+        private suspend fun getTopicsAsyncInternal(
+            getTopicsRequest: android.adservices.topics.GetTopicsRequest
+        ): android.adservices.topics.GetTopicsResponse = suspendCancellableCoroutine { continuation
+            ->
+            mTopicsManager.getTopics(
+                getTopicsRequest,
+                Runnable::run,
+                continuation.asOutcomeReceiver()
+            )
+        }
+
+        private fun convertRequest(
+            request: GetTopicsRequest
+        ): android.adservices.topics.GetTopicsRequest {
+            if (!request.shouldRecordObservation) {
+                throw IllegalArgumentException("shouldRecordObservation not supported yet.")
+            }
+            return android.adservices.topics.GetTopicsRequest.Builder()
+                .setAdsSdkName(request.sdkName)
+                .build()
+        }
+
+        internal fun convertResponse(
+            response: android.adservices.topics.GetTopicsResponse
+        ): GetTopicsResponse {
+            var topics = mutableListOf<Topic>()
+            for (topic in response.topics) {
+                topics.add(Topic(topic.taxonomyVersion, topic.modelVersion, topic.topicId))
+            }
+            return GetTopicsResponse(topics)
+        }
+    }
+
+    companion object {
+        /**
+         *  Creates [TopicsManager].
+         *
+         *  @return TopicsManagerCompat object. If the device is running an incompatible
+         *  build, the value returned is null.
+         */
+        @JvmStatic
+        @SuppressLint("NewApi", "ClassVerificationFailure")
+        fun obtain(context: Context): TopicsManager? {
+            return if (AdServicesInfo.version() >= 4) {
+                Api33Ext4Impl(context)
+            } else {
+                null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt b/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt
index e6f50d0..aa2ae90 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/current.txt
@@ -1 +1,15 @@
 // Signature format: 4.0
+package androidx.privacysandbox.sdkruntime.client {
+
+  public final class SdkSandboxManagerCompat {
+    method public static androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat from(android.content.Context context);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public suspend Object? loadSdk(String sdkName, android.os.Bundle params, kotlin.coroutines.Continuation<? super androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat>) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
+    field public static final androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat.Companion Companion;
+  }
+
+  public static final class SdkSandboxManagerCompat.Companion {
+    method public androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt b/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt
index e6f50d0..aa2ae90 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/public_plus_experimental_current.txt
@@ -1 +1,15 @@
 // Signature format: 4.0
+package androidx.privacysandbox.sdkruntime.client {
+
+  public final class SdkSandboxManagerCompat {
+    method public static androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat from(android.content.Context context);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public suspend Object? loadSdk(String sdkName, android.os.Bundle params, kotlin.coroutines.Continuation<? super androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat>) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
+    field public static final androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat.Companion Companion;
+  }
+
+  public static final class SdkSandboxManagerCompat.Companion {
+    method public androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt b/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt
index e6f50d0..aa2ae90 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/api/restricted_current.txt
@@ -1 +1,15 @@
 // Signature format: 4.0
+package androidx.privacysandbox.sdkruntime.client {
+
+  public final class SdkSandboxManagerCompat {
+    method public static androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat from(android.content.Context context);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public suspend Object? loadSdk(String sdkName, android.os.Bundle params, kotlin.coroutines.Continuation<? super androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat>) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
+    field public static final androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat.Companion Companion;
+  }
+
+  public static final class SdkSandboxManagerCompat.Companion {
+    method public androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index 4ce9b50..8f55312 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -24,9 +24,40 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    api(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core-ktx:1.8.0")
+
+    api project(path: ':privacysandbox:sdkruntime:sdkruntime-core')
+
+    implementation("androidx.core:core:1.8.0")
+
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
+    testImplementation project(":room:room-compiler-processing-testing")
+
+    // TODO(b/249982004): cleanup dependencies
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(project(":internal-testutils-truth")) // for assertThrows
+
+    androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
 }
 
 android {
+    sourceSets {
+        androidTest {
+            assets {
+                srcDirs += "src/androidTest/assets"
+            }
+        }
+    }
+    compileSdk = 33
+    compileSdkExtension = 4
     namespace "androidx.privacysandbox.sdkruntime.client"
 }
 
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3e3ea90
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml
new file mode 100644
index 0000000..d589762
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdkTable.xml
@@ -0,0 +1,25 @@
+<!--
+  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.
+  -->
+<runtime-enabled-sdk-table>
+    <runtime-enabled-sdk>
+        <compat-config-path>RuntimeEnabledSdks/V1/CompatSdkConfig.xml</compat-config-path>
+        <package-name>androidx.privacysandbox.sdkruntime.test.v1</package-name>
+    </runtime-enabled-sdk>
+    <runtime-enabled-sdk>
+        <compat-config-path>RuntimeEnabledSdks/InvalidEntryPointSdkConfig.xml</compat-config-path>
+        <package-name>androidx.privacysandbox.sdkruntime.test.invalidEntryPoint</package-name>
+    </runtime-enabled-sdk>
+</runtime-enabled-sdk-table>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/InvalidEntryPointSdkConfig.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/InvalidEntryPointSdkConfig.xml
new file mode 100644
index 0000000..d28649c
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/InvalidEntryPointSdkConfig.xml
@@ -0,0 +1,19 @@
+<!--
+  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.
+  -->
+<compat-config>
+    <compat-entrypoint>InvalidEntryPoint</compat-entrypoint>
+    <dex-path>RuntimeEnabledSdks/V1/classes.dex</dex-path>
+</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/RPackage.dex b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/RPackage.dex
new file mode 100644
index 0000000..d4862a1
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/RPackage.dex
Binary files differ
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/RPackage.md b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/RPackage.md
new file mode 100644
index 0000000..5c1e2a0
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/RPackage.md
@@ -0,0 +1,8 @@
+RPackage class for testing SDK resource remapping.
+
+RPackage.dex built from:
+
+1) androidx.privacysandbox.sdkruntime.test.RPackage
+   public class RPackage {
+      public static int packageId = 0;
+   }
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkCode.md b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkCode.md
new file mode 100644
index 0000000..a2f1cbf
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkCode.md
@@ -0,0 +1,125 @@
+Test sdk that was built with V1 library.
+
+DO NOT RECOMPILE WITH ANY CHANGES TO LIBRARY CLASSES.
+Main purpose of that provider is to test that old core versions could be loaded by new client.
+
+classes.dex built from:
+
+1) androidx.privacysandbox.sdkruntime.core.Versions
+@Keep
+object Versions {
+
+    const val API_VERSION = 1
+
+    @JvmField
+    var CLIENT_VERSION = -1
+
+    @JvmStatic
+    fun handShake(clientVersion: Int): Int {
+        CLIENT_VERSION = clientVersion
+        return API_VERSION
+    }
+}
+
+2) androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+abstract class SandboxedSdkProviderCompat {
+    var context: Context? = null
+        private set
+
+    fun attachContext(context: Context) {
+        check(this.context == null) { "Context already set" }
+        this.context = context
+    }
+
+    @Throws(LoadSdkCompatException::class)
+    abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+
+    open fun beforeUnloadSdk() {}
+
+    abstract fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+    ): View
+}
+
+3) androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+sealed class SandboxedSdkCompat {
+
+    abstract fun getInterface(): IBinder?
+
+    private class CompatImpl(private val mInterface: IBinder) : SandboxedSdkCompat() {
+        override fun getInterface(): IBinder? {
+            return mInterface
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        fun create(binder: IBinder): SandboxedSdkCompat {
+            return CompatImpl(binder)
+        }
+    }
+}
+
+4) androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+class LoadSdkCompatException : Exception {
+
+    val loadSdkErrorCode: Int
+
+    val extraInformation: Bundle
+
+    @JvmOverloads
+    constructor(
+            loadSdkErrorCode: Int,
+            message: String?,
+            cause: Throwable?,
+            extraInformation: Bundle = Bundle()
+    ) : super(message, cause) {
+        this.loadSdkErrorCode = loadSdkErrorCode
+        this.extraInformation = extraInformation
+    }
+
+    constructor(
+            cause: Throwable,
+            extraInfo: Bundle
+    ) : this(LOAD_SDK_SDK_DEFINED_ERROR, "", cause, extraInfo)
+
+    companion object {
+        const val LOAD_SDK_SDK_DEFINED_ERROR = 102
+    }
+}
+
+5) androidx.privacysandbox.sdkruntime.test.v1.CompatProvider
+class CompatProvider : SandboxedSdkProviderCompat() {
+
+    @JvmField
+    val onLoadSdkBinder = Binder()
+
+    @JvmField
+    var lastOnLoadSdkParams: Bundle? = null
+
+    @JvmField
+    var isBeforeUnloadSdkCalled = false
+
+    @Throws(LoadSdkCompatException::class)
+    override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+        lastOnLoadSdkParams = params
+        if (params.getBoolean("needFail", false)) {
+            throw LoadSdkCompatException(RuntimeException(), params)
+        }
+        return SandboxedSdkCompat.create(onLoadSdkBinder)
+    }
+
+    override fun beforeUnloadSdk() {
+        isBeforeUnloadSdkCalled = true
+    }
+
+    override fun getView(
+            windowContext: Context, params: Bundle, width: Int,
+            height: Int
+    ): View {
+        return View(windowContext)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml
new file mode 100644
index 0000000..dfeca90
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/CompatSdkConfig.xml
@@ -0,0 +1,20 @@
+<!--
+  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.
+  -->
+<compat-config>
+    <compat-entrypoint>androidx.privacysandbox.sdkruntime.test.v1.CompatProvider</compat-entrypoint>
+    <dex-path>RuntimeEnabledSdks/V1/classes.dex</dex-path>
+    <java-resources-root-path>RuntimeEnabledSdks/V1/javaresources</java-resources-root-path>
+</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/classes.dex b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/classes.dex
new file mode 100644
index 0000000..a6da7e9
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/classes.dex
Binary files differ
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/javaresources/test.txt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/javaresources/test.txt
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/V1/javaresources/test.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt
new file mode 100644
index 0000000..aff7553
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatSandboxedTest.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.privacysandbox.sdkruntime.client
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.LoadSdkException
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SdkSandboxManager
+import android.content.Context
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions.AD_SERVICES
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.test.core.app.ApplicationProvider
+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 kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/249982507) Test should be rewritten to use real SDK in sandbox instead of mocking manager
+// TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+@SuppressLint("NewApi")
+// TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+@RequiresExtension(extension = AD_SERVICES, version = 4)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+class SdkSandboxManagerCompatSandboxedTest {
+
+    private lateinit var mContext: Context
+
+    @Before
+    fun setUp() {
+        assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
+        mContext = Mockito.spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    fun loadSdk_whenNoLocalSdkExistsAndSandboxAvailable_delegateToPlatformLoadSdk() {
+        val sdkSandboxManager = mockSandboxManager(mContext)
+        setupLoadSdkAnswer(sdkSandboxManager, SandboxedSdk(Binder()))
+
+        val managerCompat = SdkSandboxManagerCompat.from(mContext)
+        val sdkName = "test"
+        val params = Bundle()
+
+        runBlocking {
+            managerCompat.loadSdk(sdkName, params)
+        }
+
+        verify(sdkSandboxManager).loadSdk(
+            eq(sdkName),
+            eq(params),
+            any(),
+            any()
+        )
+    }
+
+    @Test
+    fun loadSdk_whenNoLocalSdkExistsAndSandboxAvailable_returnResultFromPlatformLoadSdk() {
+        val sdkSandboxManager = mockSandboxManager(mContext)
+
+        val sandboxedSdk = SandboxedSdk(Binder())
+        setupLoadSdkAnswer(sdkSandboxManager, sandboxedSdk)
+
+        val managerCompat = SdkSandboxManagerCompat.from(mContext)
+
+        val result = runBlocking {
+            managerCompat.loadSdk("test", Bundle())
+        }
+
+        assertThat(result.getInterface()).isEqualTo(sandboxedSdk.getInterface())
+    }
+
+    @Test
+    fun loadSdk_whenNoLocalSdkExistsAndSandboxAvailable_rethrowsExceptionFromPlatformLoadSdk() {
+        val sdkSandboxManager = mockSandboxManager(mContext)
+
+        val loadSdkException = LoadSdkException(
+            RuntimeException(),
+            Bundle()
+        )
+        setupLoadSdkAnswer(sdkSandboxManager, loadSdkException)
+
+        val managerCompat = SdkSandboxManagerCompat.from(mContext)
+
+        val result = assertThrows(LoadSdkCompatException::class.java) {
+            runBlocking {
+                managerCompat.loadSdk("test", Bundle())
+            }
+        }
+
+        assertThat(result.cause).isEqualTo(loadSdkException.cause)
+        assertThat(result.extraInformation).isEqualTo(loadSdkException.extraInformation)
+        assertThat(result.loadSdkErrorCode).isEqualTo(loadSdkException.loadSdkErrorCode)
+    }
+
+    companion object SandboxApi {
+
+        private fun isSandboxApiAvailable() =
+            AdServicesInfo.version() >= 4
+
+        private fun mockSandboxManager(spyContext: Context): SdkSandboxManager {
+            val sdkSandboxManager = Mockito.mock(SdkSandboxManager::class.java)
+            `when`(spyContext.getSystemService(SdkSandboxManager::class.java))
+                .thenReturn(sdkSandboxManager)
+            return sdkSandboxManager
+        }
+
+        private fun setupLoadSdkAnswer(
+            sdkSandboxManager: SdkSandboxManager,
+            sandboxedSdk: SandboxedSdk
+        ) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<SandboxedSdk, LoadSdkException>>(3)
+                receiver.onResult(sandboxedSdk)
+                null
+            }
+            doAnswer(answer)
+                .`when`(sdkSandboxManager).loadSdk(
+                    any(),
+                    any(),
+                    any(),
+                    any()
+                )
+        }
+
+        private fun setupLoadSdkAnswer(
+            sdkSandboxManager: SdkSandboxManager,
+            loadSdkException: LoadSdkException
+        ) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<SandboxedSdk, LoadSdkException>>(3)
+                receiver.onError(loadSdkException)
+                null
+            }
+            doAnswer(answer)
+                .`when`(sdkSandboxManager).loadSdk(
+                    any(),
+                    any(),
+                    any(),
+                    any()
+                )
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
new file mode 100644
index 0000000..fdf6281
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompatTest.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.privacysandbox.sdkruntime.client
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Build
+import android.os.Bundle
+import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_INTERNAL_ERROR
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_SDK_DEFINED_ERROR
+import androidx.test.core.app.ApplicationProvider
+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 kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.any
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SdkSandboxManagerCompatTest {
+
+    @Test
+    fun from_whenCalledOnSameContext_returnSameManager() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+        val managerCompat2 = SdkSandboxManagerCompat.from(context)
+
+        assertThat(managerCompat2).isSameInstanceAs(managerCompat)
+    }
+
+    @Test
+    fun from_whenCalledOnDifferentContext_returnDifferentManager() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val context2 = ContextWrapper(context)
+
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+        val managerCompat2 = SdkSandboxManagerCompat.from(context2)
+
+        assertThat(managerCompat2).isNotSameInstanceAs(managerCompat)
+    }
+
+    @Test
+    // TODO(b/249982507) DexmakerMockitoInline requires P+. Rewrite to support P-
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+    fun loadSdk_whenNoLocalSdkExistsAndSandboxNotAvailable_notDelegateToSandbox() {
+        // TODO(b/262577044) Replace with @SdkSuppress after supporting maxExtensionVersion
+        assumeTrue("Requires Sandbox API not available", isSandboxApiNotAvailable())
+
+        val context = spy(ApplicationProvider.getApplicationContext<Context>())
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+
+        assertThrows(LoadSdkCompatException::class.java) {
+            runBlocking {
+                managerCompat.loadSdk("sdk-not-exists", Bundle())
+            }
+        }
+
+        verify(context, Mockito.never()).getSystemService(any())
+    }
+
+    @Test
+    fun loadSdk_whenNoLocalSdkExistsAndSandboxNotAvailable_throwsSdkNotFoundException() {
+        // TODO(b/262577044) Replace with @SdkSuppress after supporting maxExtensionVersion
+        assumeTrue("Requires Sandbox API not available", isSandboxApiNotAvailable())
+
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+
+        val result = assertThrows(LoadSdkCompatException::class.java) {
+            runBlocking {
+                managerCompat.loadSdk("sdk-not-exists", Bundle())
+            }
+        }
+
+        assertThat(result.loadSdkErrorCode)
+            .isEqualTo(LoadSdkCompatException.LOAD_SDK_NOT_FOUND)
+    }
+
+    @Test
+    fun loadSdk_whenLocalSdkExists_returnResultFromCompatLoadSdk() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+
+        val result = runBlocking {
+            managerCompat.loadSdk("androidx.privacysandbox.sdkruntime.test.v1", Bundle())
+        }
+
+        assertThat(result.getInterface()!!.javaClass.classLoader)
+            .isNotSameInstanceAs(managerCompat.javaClass.classLoader)
+    }
+
+    @Test
+    fun loadSdk_whenLocalSdkExists_rethrowsExceptionFromCompatLoadSdk() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+
+        val params = Bundle()
+        params.putBoolean("needFail", true)
+
+        val result = assertThrows(LoadSdkCompatException::class.java) {
+            runBlocking {
+                managerCompat.loadSdk("androidx.privacysandbox.sdkruntime.test.v1", params)
+            }
+        }
+
+        assertThat(result.extraInformation).isEqualTo(params)
+        assertThat(result.loadSdkErrorCode).isEqualTo(LOAD_SDK_SDK_DEFINED_ERROR)
+    }
+
+    @Test
+    fun loadSdk_whenLocalSdkFailedToLoad_throwsInternalErrorException() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val managerCompat = SdkSandboxManagerCompat.from(context)
+
+        val result = assertThrows(LoadSdkCompatException::class.java) {
+            runBlocking {
+                managerCompat.loadSdk(
+                    sdkName = "androidx.privacysandbox.sdkruntime.test.invalidEntryPoint",
+                    params = Bundle()
+                )
+            }
+        }
+
+        assertThat(result.loadSdkErrorCode).isEqualTo(LOAD_SDK_INTERNAL_ERROR)
+        assertThat(result.message).isEqualTo("Failed to instantiate local SDK")
+    }
+
+    private fun isSandboxApiNotAvailable() =
+        AdServicesInfo.version() < 4
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt
new file mode 100644
index 0000000..541d137
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParserTest.kt
@@ -0,0 +1,247 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfigParser.Companion.parse
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.ByteArrayInputStream
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.xmlpull.v1.XmlPullParserException
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LocalSdkConfigParserTest {
+
+    @Test
+    fun parse_skipUnknownTagsAndReturnParsedResult() {
+        val xml = """
+            <compat-config>
+                <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                <unknown-tag>new parameter from future library version</unknown-tag>
+                <dex-path>1.dex</dex-path>
+                <future-version-tag>
+                    <unknown-tag>new inner tag</unknown-tag>
+                </future-version-tag>
+                <dex-path>2.dex</dex-path>
+                <java-resources-root-path>javaResPath/</java-resources-root-path>
+                <resource-id-remapping>
+                    <unknown-tag>new remapping tag</unknown-tag>
+                    <r-package-class>com.test.sdk.RPackage</r-package-class>
+                    <resources-package-id>42</resources-package-id>
+                </resource-id-remapping>
+            </compat-config>
+        """.trimIndent()
+
+        val result = tryParse(xml, packageName = "com.test.sdk.package")
+
+        assertThat(result)
+            .isEqualTo(
+                LocalSdkConfig(
+                    packageName = "com.test.sdk.package",
+                    dexPaths = listOf("1.dex", "2.dex"),
+                    entryPoint = "compat.sdk.provider",
+                    javaResourcesRoot = "javaResPath/",
+                    resourceRemapping = ResourceRemappingConfig(
+                        rPackageClassName = "com.test.sdk.RPackage",
+                        packageId = 42
+                    )
+                )
+            )
+    }
+
+    @Test
+    fun parse_whenOnlyMandatoryElements_returnParsedResult() {
+        val xml = """
+            <compat-config>
+                <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                <dex-path>1.dex</dex-path>
+            </compat-config>
+        """.trimIndent()
+
+        val result = tryParse(xml, packageName = "com.test.sdk.package")
+
+        assertThat(result)
+            .isEqualTo(
+                LocalSdkConfig(
+                    packageName = "com.test.sdk.package",
+                    dexPaths = listOf("1.dex"),
+                    entryPoint = "compat.sdk.provider",
+                    javaResourcesRoot = null,
+                    resourceRemapping = null
+                )
+            )
+    }
+
+    @Test
+    fun parse_whenNoEntryPoint_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <dex-path>1.dex</dex-path>
+                </compat-config>
+            """.trimIndent(),
+            reason = "No compat-entrypoint tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenDuplicateEntryPoint_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <compat-entrypoint>compat.sdk.provider2</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                </compat-config>
+            """.trimIndent(),
+            reason = "Duplicate compat-entrypoint tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenNoDexPath_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                </compat-config>
+            """.trimIndent(),
+            reason = "No dex-path tags found"
+        )
+    }
+
+    @Test
+    fun parse_whenDuplicateJavaResourceRoot_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                    <java-resources-root-path>path1/</java-resources-root-path>
+                    <java-resources-root-path>path2/</java-resources-root-path>
+                </compat-config>
+            """.trimIndent(),
+            reason = "Duplicate java-resources-root-path tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenDuplicateResourceRemapping_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                    <resource-id-remapping>
+                        <r-package-class>com.test.sdk.RPackage</r-package-class>
+                        <resources-package-id>42</resources-package-id>
+                    </resource-id-remapping>
+                    <resource-id-remapping>
+                        <r-package-class>com.test.sdk.RPackage</r-package-class>
+                        <resources-package-id>42</resources-package-id>
+                    </resource-id-remapping>
+                </compat-config>
+            """.trimIndent(),
+            reason = "Duplicate resource-id-remapping tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenNoClassInResourceRemapping_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                    <resource-id-remapping>
+                        <resources-package-id>42</resources-package-id>
+                    </resource-id-remapping>
+                </compat-config>
+            """.trimIndent(),
+            reason = "No r-package-class tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenDuplicateClassInResourceRemapping_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                    <resource-id-remapping>
+                        <r-package-class>com.test.sdk.RPackage</r-package-class>
+                        <r-package-class>com.test.sdk.RPackage</r-package-class>
+                        <resources-package-id>42</resources-package-id>
+                    </resource-id-remapping>
+                </compat-config>
+            """.trimIndent(),
+            reason = "Duplicate r-package-class tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenNoPackageIdInResourceRemapping_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                    <resource-id-remapping>
+                        <r-package-class>com.test.sdk.RPackage</r-package-class>
+                    </resource-id-remapping>
+                </compat-config>
+            """.trimIndent(),
+            reason = "No resources-package-id tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenDuplicatePackageIdInResourceRemapping_throwsException() {
+        assertParsingFailWithReason(
+            xml = """
+                <compat-config>
+                    <compat-entrypoint>compat.sdk.provider</compat-entrypoint>
+                    <dex-path>1.dex</dex-path>
+                    <resource-id-remapping>
+                        <r-package-class>com.test.sdk.RPackage</r-package-class>
+                        <resources-package-id>42</resources-package-id>
+                        <resources-package-id>42</resources-package-id>
+                    </resource-id-remapping>
+                </compat-config>
+            """.trimIndent(),
+            reason = "Duplicate resources-package-id tag found"
+        )
+    }
+
+    private fun assertParsingFailWithReason(xml: String, reason: String) {
+        assertThrows<XmlPullParserException> {
+            tryParse(xml)
+        }.hasMessageThat().isEqualTo(
+            reason
+        )
+    }
+
+    private fun tryParse(xml: String, packageName: String = "sdkPackageName"): LocalSdkConfig {
+        ByteArrayInputStream(xml.toByteArray()).use { inputStream ->
+            return parse(inputStream, packageName)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
new file mode 100644
index 0000000..0b51558
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LocalSdkConfigsHolderTest {
+
+    @Test
+    fun load_whenSdkTableNotExists_doesNotThrowException() {
+        val configHolder = LocalSdkConfigsHolder.load(
+            ApplicationProvider.getApplicationContext(),
+            sdkTableAssetName = "not-exists"
+        )
+        val result = configHolder.getSdkConfig("sdk")
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun getSdkConfig_whenSdkExists_returnSdkInfo() {
+        val configHolder = LocalSdkConfigsHolder.load(
+            ApplicationProvider.getApplicationContext()
+        )
+
+        val result = configHolder.getSdkConfig(
+            "androidx.privacysandbox.sdkruntime.test.v1"
+        )
+
+        assertThat(result)
+            .isEqualTo(
+                LocalSdkConfig(
+                    packageName = "androidx.privacysandbox.sdkruntime.test.v1",
+                    dexPaths = listOf("RuntimeEnabledSdks/V1/classes.dex"),
+                    entryPoint = "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider",
+                    javaResourcesRoot = "RuntimeEnabledSdks/V1/javaresources"
+                )
+            )
+    }
+
+    @Test
+    fun getSdkConfig_whenSdkNotExists_returnNull() {
+        val configHolder = LocalSdkConfigsHolder.load(
+            ApplicationProvider.getApplicationContext()
+        )
+
+        val result = configHolder.getSdkConfig("not-exists")
+
+        assertThat(result).isNull()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/SdkTableConfigParserTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/SdkTableConfigParserTest.kt
new file mode 100644
index 0000000..e646377
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/SdkTableConfigParserTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+import androidx.privacysandbox.sdkruntime.client.config.SdkTableConfigParser.Companion.parse
+import androidx.privacysandbox.sdkruntime.client.config.SdkTableConfigParser.SdkTableEntry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.ByteArrayInputStream
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.xmlpull.v1.XmlPullParserException
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SdkTableConfigParserTest {
+
+    @Test
+    fun parse_skipUnknownTagsAndReturnSetWithSdkTableEntries() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+                <runtime-enabled-sdk>
+                    <package-name>sdk1</package-name>
+                    <unknown-tag>new parameter from future library version</unknown-tag>
+                    <compat-config-path>config1.xml</compat-config-path>
+                </runtime-enabled-sdk>
+                <future-version-runtime-enabled-sdk>
+                    <unknown-tag>new sdk type without old tags</unknown-tag>
+                </future-version-runtime-enabled-sdk>
+                <runtime-enabled-sdk>
+                    <unknown-tag2>new parameter from future library version</unknown-tag2>
+                    <package-name>sdk2</package-name>
+                    <compat-config-path>config2.xml</compat-config-path>
+                </runtime-enabled-sdk>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        val result = tryParse(xml)
+
+        assertThat(result)
+            .containsExactly(
+                SdkTableEntry("sdk1", "config1.xml"),
+                SdkTableEntry("sdk2", "config2.xml")
+            )
+    }
+
+    @Test
+    fun parse_whenEmptyTable_returnsEmptyMap() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        val result = tryParse(xml)
+
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun parse_whenNoPackageName_throwsException() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+                <runtime-enabled-sdk>
+                    <compat-config-path>config1.xml</compat-config-path>
+                </runtime-enabled-sdk>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        assertThrows<XmlPullParserException> {
+            tryParse(xml)
+        }.hasMessageThat().isEqualTo(
+            "No package-name tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenMultiplePackageNames_throwsException() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+                <runtime-enabled-sdk>
+                    <package-name>sdk1</package-name>
+                    <package-name>sdk2</package-name>
+                    <compat-config-path>config1.xml</compat-config-path>
+                </runtime-enabled-sdk>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        assertThrows<XmlPullParserException> {
+            tryParse(xml)
+        }.hasMessageThat().isEqualTo(
+            "Duplicate package-name tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenNoConfigPath_throwsException() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+                <runtime-enabled-sdk>
+                    <package-name>sdk1</package-name>
+                </runtime-enabled-sdk>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        assertThrows<XmlPullParserException> {
+            tryParse(xml)
+        }.hasMessageThat().isEqualTo(
+            "No compat-config-path tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenMultipleConfigPaths_throwsException() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+                <runtime-enabled-sdk>
+                    <package-name>sdk1</package-name>
+                    <compat-config-path>config1.xml</compat-config-path>
+                    <compat-config-path>config2.xml</compat-config-path>
+                </runtime-enabled-sdk>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        assertThrows<XmlPullParserException> {
+            tryParse(xml)
+        }.hasMessageThat().isEqualTo(
+            "Duplicate compat-config-path tag found"
+        )
+    }
+
+    @Test
+    fun parse_whenDuplicatePackageName_throwsException() {
+        val xml = """
+            <runtime-enabled-sdk-table>
+                <runtime-enabled-sdk>
+                    <package-name>sdk1</package-name>
+                    <compat-config-path>config1.xml</compat-config-path>
+                </runtime-enabled-sdk>
+                <runtime-enabled-sdk>
+                    <package-name>sdk1</package-name>
+                    <compat-config-path>config2.xml</compat-config-path>
+                </runtime-enabled-sdk>
+            </runtime-enabled-sdk-table>
+        """.trimIndent()
+
+        assertThrows<XmlPullParserException> {
+            tryParse(xml)
+        }.hasMessageThat().isEqualTo(
+            "Duplicate entry for sdk1 found"
+        )
+    }
+
+    private fun tryParse(xml: String): Set<SdkTableEntry> {
+        ByteArrayInputStream(xml.toByteArray()).use { inputStream ->
+            return parse(inputStream)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/FileClassLoaderFactoryTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/FileClassLoaderFactoryTest.kt
new file mode 100644
index 0000000..31fee55
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/FileClassLoaderFactoryTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.client.loader.storage.LocalSdkStorage
+import androidx.privacysandbox.sdkruntime.client.loader.storage.LocalSdkDexFiles
+import androidx.privacysandbox.sdkruntime.client.loader.storage.TestLocalSdkStorage
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FileClassLoaderFactoryTest {
+
+    private lateinit var testSdkConfig: LocalSdkConfig
+
+    @Before
+    fun setUp() {
+        testSdkConfig = LocalSdkConfig(
+            packageName = "androidx.privacysandbox.sdkruntime.test.v1",
+            dexPaths = listOf(
+                "RuntimeEnabledSdks/V1/classes.dex",
+            ),
+            entryPoint = "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider",
+        )
+    }
+
+    @Test
+    fun createClassLoaderFor_whenSdkStorageReturnFiles_returnClassloaderAndNotDelegateToFallback() {
+        val sdkDexFiles = extractTestSdkDexFiles()
+        val fallback = TestFallbackFactory()
+
+        val fileClassLoaderFactory = FileClassLoaderFactory(
+            StubSdkStorage(result = sdkDexFiles),
+            fallback
+        )
+
+        val classLoader = fileClassLoaderFactory.createClassLoaderFor(
+            testSdkConfig,
+            javaClass.classLoader!!.parent!!
+        )
+
+        val loadedEntryPointClass = classLoader.loadClass(testSdkConfig.entryPoint)
+        assertThat(loadedEntryPointClass.classLoader).isEqualTo(classLoader)
+
+        assertThat(fallback.loadSdkCalled).isFalse()
+    }
+
+    @Test
+    fun createClassLoaderFor_whenSdkStorageReturnNull_delegateToFallback() {
+        val fallback = TestFallbackFactory(testSdkConfig, javaClass.classLoader!!.parent)
+        val fileClassLoaderFactory = FileClassLoaderFactory(
+            StubSdkStorage(result = null),
+            fallback
+        )
+
+        fileClassLoaderFactory.createClassLoaderFor(
+            testSdkConfig,
+            javaClass.classLoader!!.parent!!
+        )
+
+        assertThat(fallback.loadSdkCalled).isTrue()
+    }
+
+    @Test
+    fun createClassLoaderFor_whenSdkStorageThrows_delegateToFallback() {
+        val fallback = TestFallbackFactory(testSdkConfig, javaClass.classLoader!!.parent)
+        val fileClassLoaderFactory = FileClassLoaderFactory(
+            ThrowingSdkStorage(exception = Exception("Something wrong")),
+            fallback
+        )
+
+        fileClassLoaderFactory.createClassLoaderFor(
+            testSdkConfig,
+            javaClass.classLoader!!.parent!!
+        )
+
+        assertThat(fallback.loadSdkCalled).isTrue()
+    }
+
+    private class StubSdkStorage(
+        private val result: LocalSdkDexFiles?
+    ) : LocalSdkStorage {
+        override fun dexFilesFor(sdkConfig: LocalSdkConfig) = result
+    }
+
+    private class ThrowingSdkStorage(
+        private val exception: Exception
+    ) : LocalSdkStorage {
+        override fun dexFilesFor(sdkConfig: LocalSdkConfig): LocalSdkDexFiles? {
+            throw exception
+        }
+    }
+
+    private class TestFallbackFactory(
+        private val expectedSdkConfig: LocalSdkConfig? = null,
+        private val expectedParent: ClassLoader? = null,
+    ) : SdkLoader.ClassLoaderFactory {
+
+        var loadSdkCalled: Boolean = false
+
+        override fun createClassLoaderFor(
+            sdkConfig: LocalSdkConfig,
+            parent: ClassLoader
+        ): ClassLoader {
+            assertThat(sdkConfig).isEqualTo(expectedSdkConfig)
+            assertThat(parent).isEqualTo(expectedParent)
+            loadSdkCalled = true
+            return parent
+        }
+    }
+
+    private fun extractTestSdkDexFiles(): LocalSdkDexFiles {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+
+        val testStorage = TestLocalSdkStorage(
+            context,
+            rootFolder = File(context.cacheDir, "FileClassLoaderFactoryTest")
+        )
+
+        return testStorage.dexFilesFor(testSdkConfig)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/InMemorySdkClassLoaderFactoryTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/InMemorySdkClassLoaderFactoryTest.kt
new file mode 100644
index 0000000..93aec37
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/InMemorySdkClassLoaderFactoryTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.os.Build
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.test.core.app.ApplicationProvider
+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.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InMemorySdkClassLoaderFactoryTest {
+
+    private lateinit var factoryUnderTest: InMemorySdkClassLoaderFactory
+    private lateinit var testSdkInfo: LocalSdkConfig
+
+    @Before
+    fun setUp() {
+        factoryUnderTest = InMemorySdkClassLoaderFactory.create(
+            ApplicationProvider.getApplicationContext()
+        )
+        testSdkInfo = LocalSdkConfig(
+            packageName = "androidx.privacysandbox.sdkruntime.test.v1",
+            dexPaths = listOf("RuntimeEnabledSdks/V1/classes.dex"),
+            entryPoint = "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider",
+            javaResourcesRoot = "RuntimeEnabledSdks/V1/"
+        )
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O_MR1)
+    fun createClassLoaderFor_whenApi27_returnClassloader() {
+        val classLoader = factoryUnderTest.createClassLoaderFor(
+            testSdkInfo,
+            javaClass.classLoader!!
+        )
+        val loadedEntryPointClass = classLoader.loadClass(testSdkInfo.entryPoint)
+        assertThat(loadedEntryPointClass.classLoader).isEqualTo(classLoader)
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O)
+    fun createClassLoaderFor_whenApiPre27_throwsSandboxDisabledException() {
+        val ex = assertThrows(LoadSdkCompatException::class.java) {
+            factoryUnderTest.createClassLoaderFor(
+                testSdkInfo,
+                javaClass.classLoader!!
+            )
+        }
+
+        assertThat(ex.loadSdkErrorCode)
+            .isEqualTo(LoadSdkCompatException.LOAD_SDK_SDK_SANDBOX_DISABLED)
+        assertThat(ex)
+            .hasMessageThat()
+            .isEqualTo("Can't use InMemoryDexClassLoader")
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactoryTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactoryTest.kt
new file mode 100644
index 0000000..3da2c8b
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactoryTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class JavaResourcesLoadingClassLoaderFactoryTest {
+
+    private lateinit var appClassloader: ClassLoader
+    private lateinit var factoryUnderTest: JavaResourcesLoadingClassLoaderFactory
+    private lateinit var testSdkConfig: LocalSdkConfig
+
+    @Before
+    fun setUp() {
+        appClassloader = javaClass.classLoader!!
+        factoryUnderTest = JavaResourcesLoadingClassLoaderFactory(
+            appClassloader,
+            codeClassLoaderFactory = object : SdkLoader.ClassLoaderFactory {
+                override fun createClassLoaderFor(sdkConfig: LocalSdkConfig, parent: ClassLoader) =
+                    parent
+            }
+        )
+        testSdkConfig = LocalSdkConfig(
+            packageName = "androidx.privacysandbox.sdkruntime.test.v1",
+            dexPaths = listOf("RuntimeEnabledSdks/V1/classes.dex"),
+            entryPoint = "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider",
+            javaResourcesRoot = "RuntimeEnabledSdks/V1/javaresources"
+        )
+    }
+
+    @Test
+    fun getResource_delegateToAppClassloaderWithPrefix() {
+        val classLoader = factoryUnderTest.createClassLoaderFor(
+            testSdkConfig,
+            appClassloader.parent!!
+        )
+        val resource = classLoader.getResource("test.txt")
+
+        val appResource = appClassloader.getResource(
+            "assets/RuntimeEnabledSdks/V1/javaresources/test.txt"
+        )
+        assertThat(resource).isNotNull()
+        assertThat(resource).isEqualTo(appResource)
+    }
+
+    @Test
+    fun getResource_whenAppResource_returnNull() {
+        val classLoader = factoryUnderTest.createClassLoaderFor(
+            testSdkConfig,
+            appClassloader.parent!!
+        )
+
+        val resource = classLoader.getResource("assets/RuntimeEnabledSdkTable.xml")
+        val appResource = appClassloader.getResource("assets/RuntimeEnabledSdkTable.xml")
+
+        assertThat(appResource).isNotNull()
+        assertThat(resource).isNull()
+    }
+
+    @Test
+    fun getResources_delegateToAppClassloaderWithPrefix() {
+        val classLoader = factoryUnderTest.createClassLoaderFor(
+            testSdkConfig,
+            appClassloader.parent!!
+        )
+
+        val resources = classLoader
+            .getResources("test.txt")
+            .toList()
+        assertThat(resources.isEmpty()).isFalse()
+
+        val appResources = appClassloader
+            .getResources("assets/RuntimeEnabledSdks/V1/javaresources/test.txt")
+            .toList()
+
+        assertThat(appResources).isEqualTo(resources)
+    }
+
+    @Test
+    fun getResources_whenAppResource_returnEmpty() {
+        val classLoader = factoryUnderTest.createClassLoaderFor(
+            testSdkConfig,
+            appClassloader.parent!!
+        )
+
+        val resources = classLoader.getResources("assets/RuntimeEnabledSdkTable.xml")
+        val appResources = appClassloader.getResources("assets/RuntimeEnabledSdkTable.xml")
+
+        assertThat(appResources.hasMoreElements()).isTrue()
+        assertThat(resources.hasMoreElements()).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTest.kt
new file mode 100644
index 0000000..37635f0
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTest.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import android.os.Binder
+import android.os.Bundle
+import android.view.View
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.client.loader.impl.SandboxedSdkContextCompat
+import androidx.privacysandbox.sdkruntime.client.loader.storage.TestLocalSdkStorage
+import androidx.privacysandbox.sdkruntime.client.loader.storage.toClassPathString
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import dalvik.system.BaseDexClassLoader
+import java.io.File
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+internal class LocalSdkTest(
+    @Suppress("unused") private val sdkPath: String,
+    @Suppress("unused") private val sdkVersion: Int,
+    private val loadedSdk: LocalSdk
+) {
+
+    @Test
+    fun loadSdk_attachCorrectContext() {
+        val sdkContext = loadedSdk.extractSdkContext()
+        assertThat(sdkContext.javaClass.name)
+            .isEqualTo(SandboxedSdkContextCompat::class.java.name)
+    }
+
+    @Test
+    fun onLoadSdk_callOnLoadSdkAndReturnResult() {
+        val params = Bundle()
+
+        val sandboxedSdkCompat = loadedSdk.onLoadSdk(params)
+
+        val expectedBinder = loadedSdk.extractSdkProviderFieldValue<Binder>(
+            fieldName = "onLoadSdkBinder",
+        )
+        assertThat(sandboxedSdkCompat.getInterface()).isEqualTo(expectedBinder)
+
+        val lastParams = loadedSdk.extractSdkProviderFieldValue<Bundle>(
+            fieldName = "lastOnLoadSdkParams",
+        )
+        assertThat(lastParams).isEqualTo(params)
+    }
+
+    @Test
+    fun onLoadSdk_callOnLoadSdkAndThrowException() {
+        val params = Bundle()
+        params.putBoolean("needFail", true)
+
+        val ex = assertThrows(LoadSdkCompatException::class.java) {
+            loadedSdk.onLoadSdk(params)
+        }
+
+        assertThat(ex.extraInformation).isEqualTo(params)
+    }
+
+    @Test
+    fun beforeUnloadSdk_callBeforeUnloadSdk() {
+        loadedSdk.beforeUnloadSdk()
+
+        val isBeforeUnloadSdkCalled = loadedSdk.extractSdkProviderFieldValue<Boolean>(
+            fieldName = "isBeforeUnloadSdkCalled"
+        )
+
+        assertThat(isBeforeUnloadSdkCalled).isTrue()
+    }
+
+    class CurrentVersionProviderLoadTest : SandboxedSdkProviderCompat() {
+        @JvmField
+        val onLoadSdkBinder = Binder()
+
+        @JvmField
+        var lastOnLoadSdkParams: Bundle? = null
+
+        @JvmField
+        var isBeforeUnloadSdkCalled = false
+
+        @Throws(LoadSdkCompatException::class)
+        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+            lastOnLoadSdkParams = params
+            if (params.getBoolean("needFail", false)) {
+                throw LoadSdkCompatException(RuntimeException(), params)
+            }
+            return SandboxedSdkCompat(onLoadSdkBinder)
+        }
+
+        override fun beforeUnloadSdk() {
+            isBeforeUnloadSdkCalled = true
+        }
+
+        override fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+        ): View {
+            return View(windowContext)
+        }
+    }
+
+    internal class TestClassLoaderFactory(
+        private val testStorage: TestLocalSdkStorage
+    ) : SdkLoader.ClassLoaderFactory {
+        override fun createClassLoaderFor(
+            sdkConfig: LocalSdkConfig,
+            parent: ClassLoader
+        ): ClassLoader {
+            val sdkDexFiles = testStorage.dexFilesFor(sdkConfig)
+
+            val optimizedDirectory = File(sdkDexFiles.files[0].parentFile, "DexOpt")
+            if (!optimizedDirectory.exists()) {
+                optimizedDirectory.mkdirs()
+            }
+
+            return BaseDexClassLoader(
+                sdkDexFiles.toClassPathString(),
+                optimizedDirectory,
+                /* librarySearchPath = */ null,
+                parent
+            )
+        }
+    }
+
+    internal class TestSdkInfo internal constructor(
+        val apiVersion: Int,
+        dexPath: String,
+        sdkProviderClass: String
+    ) {
+        val localSdkConfig = LocalSdkConfig(
+            packageName = "test.$apiVersion.$sdkProviderClass",
+            dexPaths = listOf(dexPath),
+            entryPoint = sdkProviderClass
+        )
+    }
+
+    companion object {
+        private val SDKS = arrayOf(
+            TestSdkInfo(
+                1,
+                "RuntimeEnabledSdks/V1/classes.dex",
+                "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider"
+            )
+        )
+
+        @Parameterized.Parameters(name = "sdk: {0}, version: {1}")
+        @JvmStatic
+        fun params(): List<Array<Any>> = buildList {
+            assertThat(SDKS.size).isEqualTo(Versions.API_VERSION)
+
+            val assetsSdkLoader = createAssetsSdkLoader()
+            for (i in SDKS.indices) {
+                val sdk = SDKS[i]
+                assertThat(sdk.apiVersion).isEqualTo(i + 1)
+
+                val loadedSdk = assetsSdkLoader.loadSdk(sdk.localSdkConfig)
+                assertThat(loadedSdk.extractApiVersion())
+                    .isEqualTo(sdk.apiVersion)
+
+                add(
+                    arrayOf(
+                        sdk.localSdkConfig.dexPaths[0],
+                        sdk.apiVersion,
+                        loadedSdk
+                    )
+                )
+            }
+
+            // add SDK loaded from test sources
+            add(
+                arrayOf(
+                    "BuiltFromSource",
+                    Versions.API_VERSION,
+                    loadTestSdkFromSource(),
+                )
+            )
+        }
+
+        private fun loadTestSdkFromSource(): LocalSdk {
+            val sdkLoader = SdkLoader(
+                object : SdkLoader.ClassLoaderFactory {
+                    override fun createClassLoaderFor(
+                        sdkConfig: LocalSdkConfig,
+                        parent: ClassLoader
+                    ): ClassLoader = javaClass.classLoader!!
+                },
+                ApplicationProvider.getApplicationContext()
+            )
+
+            return sdkLoader.loadSdk(
+                LocalSdkConfig(
+                    packageName = "test.CurrentVersionProviderLoadTest",
+                    dexPaths = emptyList(),
+                    entryPoint = CurrentVersionProviderLoadTest::class.java.name
+                )
+            )
+        }
+
+        private fun createAssetsSdkLoader(): SdkLoader {
+            val context = ApplicationProvider.getApplicationContext<Context>()
+            val testStorage = TestLocalSdkStorage(
+                context,
+                rootFolder = File(context.cacheDir, "LocalSdkTest")
+            )
+            return SdkLoader(
+                TestClassLoaderFactory(testStorage),
+                context
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
new file mode 100644
index 0000000..0cfa817
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdkTestUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
+import kotlin.reflect.cast
+
+/**
+ * Extract value of [Versions.API_VERSION] from loaded SDK.
+ */
+internal fun LocalSdk.extractApiVersion(): Int =
+    extractVersionValue("API_VERSION")
+
+/**
+ * Extract value of [Versions.CLIENT_VERSION] from loaded SDK.
+ */
+internal fun LocalSdk.extractClientVersion(): Int =
+    extractVersionValue("CLIENT_VERSION")
+
+/**
+ * Extract [SandboxedSdkProviderCompat.context] from loaded SDK.
+ */
+internal fun LocalSdk.extractSdkContext(): Context {
+    val getContextMethod = sdkProvider
+        .javaClass
+        .getMethod("getContext")
+
+    val rawContext = getContextMethod.invoke(sdkProvider)
+
+    return Context::class.cast(rawContext)
+}
+
+/**
+ * Extract field value from [SandboxedSdkProviderCompat]
+ */
+internal inline fun <reified T> LocalSdk.extractSdkProviderFieldValue(fieldName: String): T {
+    return sdkProvider
+        .javaClass
+        .getField(fieldName)
+        .get(sdkProvider)!! as T
+}
+
+/**
+ * Extract classloader that was used for loading of [SandboxedSdkProviderCompat].
+ */
+internal fun LocalSdk.extractSdkProviderClassloader(): ClassLoader =
+    sdkProvider.javaClass.classLoader!!
+
+private fun LocalSdk.extractVersionValue(versionFieldName: String): Int {
+    val versionsClass = Class.forName(
+        Versions::class.java.name,
+        false,
+        extractSdkProviderClassloader()
+    )
+    return versionsClass.getDeclaredField(versionFieldName).get(null) as Int
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt
new file mode 100644
index 0000000..11c62973
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SandboxedSdkContextCompatTest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.loader.impl.SandboxedSdkContextCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SandboxedSdkContextCompatTest {
+
+    @Test
+    fun getClassloader_returnSdkClassloader() {
+        val sdkClassLoader = javaClass.classLoader!!.parent!!
+
+        val sdkContextCompat = SandboxedSdkContextCompat(
+            ApplicationProvider.getApplicationContext(),
+            sdkClassLoader
+        )
+        assertThat(sdkContextCompat.classLoader)
+            .isEqualTo(sdkClassLoader)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
new file mode 100644
index 0000000..2c2b15d
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import android.os.Build
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.client.config.ResourceRemappingConfig
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.Versions
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SdkLoaderTest {
+
+    private lateinit var sdkLoader: SdkLoader
+
+    private lateinit var testSdkConfig: LocalSdkConfig
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        sdkLoader = SdkLoader.create(
+            context
+        )
+        testSdkConfig = LocalSdkConfig(
+            packageName = "androidx.privacysandbox.sdkruntime.test.v1",
+            dexPaths = listOf(
+                "RuntimeEnabledSdks/V1/classes.dex",
+                "RuntimeEnabledSdks/RPackage.dex"
+            ),
+            entryPoint = "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider",
+            javaResourcesRoot = "RuntimeEnabledSdks/V1/javaresources",
+            resourceRemapping = ResourceRemappingConfig(
+                rPackageClassName = "androidx.privacysandbox.sdkruntime.test.RPackage",
+                packageId = 42
+            )
+        )
+
+        // Clean extracted SDKs between tests
+        File(context.cacheDir, "RuntimeEnabledSdk").deleteRecursively()
+    }
+
+    @Test
+    fun loadSdk_callVersionsHandShake() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
+
+        assertThat(loadedSdk.extractClientVersion())
+            .isEqualTo(Versions.API_VERSION)
+    }
+
+    @Test
+    fun testContextClassloader() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
+
+        val classLoader = loadedSdk.extractSdkProviderClassloader()
+        val sdkContext = loadedSdk.extractSdkContext()
+
+        assertThat(sdkContext.classLoader)
+            .isSameInstanceAs(classLoader)
+    }
+
+    @Test
+    fun testJavaResources() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
+
+        val classLoader = loadedSdk.extractSdkProviderClassloader()
+        val content = classLoader.getResourceAsStream("test.txt").use { inputStream ->
+            inputStream.bufferedReader().readLine()
+        }
+
+        assertThat(content)
+            .isEqualTo("test")
+    }
+
+    @Test
+    fun testRPackageUpdate() {
+        val loadedSdk = sdkLoader.loadSdk(testSdkConfig)
+
+        val classLoader = loadedSdk.extractSdkProviderClassloader()
+
+        val rPackageClass =
+            classLoader.loadClass("androidx.privacysandbox.sdkruntime.test.RPackage")
+
+        val packageIdField = rPackageClass.getDeclaredField("packageId")
+        val value = packageIdField.get(null)
+
+        assertThat(value).isEqualTo(42)
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O)
+    fun testLowSpace_failPreApi27() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val sdkLoaderWithLowSpaceMode = SdkLoader.create(
+            context,
+            lowSpaceThreshold = Long.MAX_VALUE
+        )
+
+        assertThrows(LoadSdkCompatException::class.java) {
+            sdkLoaderWithLowSpaceMode.loadSdk(testSdkConfig)
+        }.hasMessageThat().isEqualTo("Can't use InMemoryDexClassLoader")
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O_MR1)
+    fun testLowSpace_notFailApi27() {
+        val sdkLoaderWithLowSpaceMode = SdkLoader.create(
+            ApplicationProvider.getApplicationContext(),
+            lowSpaceThreshold = Long.MAX_VALUE
+        )
+
+        val loadedSdk = sdkLoaderWithLowSpaceMode.loadSdk(testSdkConfig)
+        val classLoader = loadedSdk.extractSdkProviderClassloader()
+
+        val entryPointClass = classLoader.loadClass(testSdkConfig.entryPoint)
+        assertThat(entryPointClass).isNotNull()
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/CachedLocalSdkStorageTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/CachedLocalSdkStorageTest.kt
new file mode 100644
index 0000000..428c85f
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/CachedLocalSdkStorageTest.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.storage
+
+import android.content.Context
+import android.os.Environment
+import android.os.StatFs
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.InputStream
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CachedLocalSdkStorageTest {
+
+    private lateinit var context: Context
+
+    private lateinit var storageUnderTest: CachedLocalSdkStorage
+
+    private lateinit var testSdkConfig: LocalSdkConfig
+
+    private lateinit var sdkFolder: File
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+
+        storageUnderTest = CachedLocalSdkStorage.create(
+            context,
+            lowSpaceThreshold = disabledLowSpaceModeThreshold()
+        )
+
+        testSdkConfig = LocalSdkConfig(
+            packageName = "androidx.privacysandbox.sdkruntime.test.v1",
+            dexPaths = listOf(
+                "RuntimeEnabledSdks/V1/classes.dex",
+                "RuntimeEnabledSdks/RPackage.dex"
+            ),
+            entryPoint = "androidx.privacysandbox.sdkruntime.test.v1.CompatProvider",
+        )
+
+        sdkFolder = LocalSdkFolderProvider
+            .create(context)
+            .dexFolderFor(testSdkConfig)
+
+        // Clean up between tests
+        sdkFolder.deleteRecursively()
+        sdkFolder.deleteOnExit()
+    }
+
+    @Test
+    fun dexFilesFor_extractSdkDexFilesAndMakeThemReadOnly() {
+        val dexFiles = storageUnderTest.dexFilesFor(testSdkConfig)
+        assertThat(dexFiles).isNotNull()
+        val dexList = dexFiles!!.files
+
+        assertThat(dexList.size).isEqualTo(testSdkConfig.dexPaths.size)
+        for (index in testSdkConfig.dexPaths.indices) {
+            assertAssetExtractedToReadOnlyFile(testSdkConfig.dexPaths[index], dexList[index])
+        }
+    }
+
+    @Test
+    fun dexFilesFor_whenAlreadyExtracted_returnExistingFilesWithoutModification() {
+        val firstResult = storageUnderTest.dexFilesFor(testSdkConfig)!!.files
+        val lastModifiedBefore = firstResult[0].lastModified()
+
+        // Wait some time to check that no new files will be created (same lastModified)
+        Thread.sleep(100)
+
+        val secondResult = storageUnderTest.dexFilesFor(testSdkConfig)!!.files
+        val lastModifiedAfter = secondResult[0].lastModified()
+
+        assertThat(secondResult).isEqualTo(firstResult)
+        assertThat(lastModifiedAfter).isEqualTo(lastModifiedBefore)
+    }
+
+    @Test
+    fun dexFilesFor_whenFileMissing_extractOnlyThisFile() {
+        val firstResult = storageUnderTest.dexFilesFor(testSdkConfig)!!.files
+        val fileToDelete = firstResult[0]
+        val fileToKeep = firstResult[1]
+
+        fileToDelete.delete()
+        val fileToKeepLastModified = fileToKeep.lastModified()
+
+        // Wait some time to check that existing file will not be modified (same lastModified)
+        Thread.sleep(100)
+
+        val secondResult = storageUnderTest.dexFilesFor(testSdkConfig)!!.files
+        val extractedFile = secondResult[0]
+        val notModifiedFile = secondResult[1]
+
+        assertThat(secondResult).isEqualTo(firstResult)
+        assertAssetExtractedToReadOnlyFile(testSdkConfig.dexPaths[0], extractedFile)
+        assertThat(notModifiedFile.lastModified()).isEqualTo(fileToKeepLastModified)
+    }
+
+    @Test
+    fun dexFilesFor_whenLowSpaceAndNoExtractedFiles_returnNull() {
+        val storageWithLowSpaceEnabled = CachedLocalSdkStorage.create(
+            context,
+            lowSpaceThreshold = enabledLowSpaceModeThreshold()
+        )
+        val result = storageWithLowSpaceEnabled.dexFilesFor(testSdkConfig)
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun dexFilesFor_whenLowSpaceAndFilesExtractedBefore_returnExistingFiles() {
+        val extractedFiles = storageUnderTest.dexFilesFor(testSdkConfig)
+        assertThat(extractedFiles).isNotNull()
+
+        val storageWithLowSpaceEnabled = CachedLocalSdkStorage.create(
+            context,
+            lowSpaceThreshold = enabledLowSpaceModeThreshold()
+        )
+        val result = storageWithLowSpaceEnabled.dexFilesFor(testSdkConfig)
+        assertThat(result).isEqualTo(extractedFiles)
+    }
+
+    @Test
+    fun dexFilesFor_whenFailedToExtract_deleteFolderAndThrowException() {
+        val invalidConfig = LocalSdkConfig(
+            packageName = "storage.test.invalid.dexPath",
+            dexPaths = listOf("NOT_EXISTS"),
+            entryPoint = "EntryPoint"
+        )
+
+        val rootFolder = LocalSdkFolderProvider
+            .create(context)
+            .dexFolderFor(invalidConfig)
+
+        val fileToDelete = File(rootFolder, "toDelete")
+        fileToDelete.createNewFile()
+        assertThat(fileToDelete.exists()).isTrue()
+
+        assertThrows<FileNotFoundException> {
+            storageUnderTest.dexFilesFor(invalidConfig)
+        }.hasMessageThat().contains("NOT_EXISTS")
+
+        assertThat(fileToDelete.exists()).isFalse()
+    }
+
+    private fun assertAssetExtractedToReadOnlyFile(assetPath: String, outputFile: File) {
+        assertThat(outputFile.exists()).isTrue()
+        assertThat(outputFile.parentFile).isEqualTo(sdkFolder)
+        assertThat(outputFile.canWrite()).isFalse()
+
+        val assetContent = context.assets.open(assetPath).use(InputStream::readBytes)
+        val fileContent = outputFile.inputStream().use(InputStream::readBytes)
+        assertThat(fileContent).isEqualTo(assetContent)
+    }
+
+    private fun enabledLowSpaceModeThreshold(): Long =
+        availableBytes() + 10_000_000
+
+    private fun disabledLowSpaceModeThreshold(): Long =
+        availableBytes() - 10_000_000
+
+    @Suppress("DEPRECATION")
+    private fun availableBytes(): Long {
+        val path = Environment.getDataDirectory()
+        val stat = StatFs(path.path)
+        val blockSize = stat.blockSize.toLong()
+        val availableBlocks = stat.availableBlocks.toLong()
+        return availableBlocks * blockSize
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkFolderProviderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkFolderProviderTest.kt
new file mode 100644
index 0000000..8e06bf7
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkFolderProviderTest.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.storage
+
+import android.content.Context
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LocalSdkFolderProviderTest {
+
+    private lateinit var context: Context
+
+    private lateinit var sdkRootFolder: File
+    private lateinit var versionFile: File
+
+    private var appLastUpdateTime = 0L
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+
+        sdkRootFolder = File(context.cacheDir, "RuntimeEnabledSdk")
+        versionFile = File(sdkRootFolder, "Folder.version")
+
+        @Suppress("DEPRECATION")
+        appLastUpdateTime =
+            context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
+
+        sdkRootFolder.deleteRecursively()
+    }
+
+    @Test
+    fun create_whenNoSdkRootFolder_createSdkRootFolderAndVersionFile() {
+        LocalSdkFolderProvider.create(context)
+
+        assertThat(sdkRootFolder.exists()).isTrue()
+        val version = readVersionFromFile()
+        assertThat(version).isEqualTo(appLastUpdateTime)
+    }
+
+    @Test
+    fun create_whenVersionNotChanged_doNotRemoveContents() {
+        // Initial create
+        LocalSdkFolderProvider.create(context)
+        val fileToKeep = File(sdkRootFolder, "file")
+        fileToKeep.createNewFile()
+
+        LocalSdkFolderProvider.create(context)
+
+        assertThat(fileToKeep.exists()).isTrue()
+    }
+
+    @Test
+    fun create_whenVersionChanged_deleteSdkRootFolderContentAndCreateVersionFile() {
+        val fileToDelete = createFileToDeleteInSdkRootFolder()
+        createVersionFile {
+            it.writeLong(42)
+        }
+
+        LocalSdkFolderProvider.create(context)
+
+        assertThat(fileToDelete.exists()).isFalse()
+        val version = readVersionFromFile()
+        assertThat(version).isEqualTo(appLastUpdateTime)
+    }
+
+    @Test
+    fun create_whenNoVersionFile_deleteSdkRootFolderContentAndCreateVersionFile() {
+        val fileToDelete = createFileToDeleteInSdkRootFolder()
+
+        LocalSdkFolderProvider.create(context)
+
+        assertThat(sdkRootFolder.exists()).isTrue()
+        assertThat(fileToDelete.exists()).isFalse()
+
+        val version = readVersionFromFile()
+        assertThat(version).isEqualTo(appLastUpdateTime)
+    }
+
+    @Test
+    fun create_whenInvalidVersionFile_deleteSdkRootFolderContentAndCreateVersionFile() {
+        val fileToDelete = createFileToDeleteInSdkRootFolder()
+        createVersionFile {
+            // Version is long type, byte is not enough
+            it.writeByte(1)
+        }
+
+        LocalSdkFolderProvider.create(context)
+
+        assertThat(fileToDelete.exists()).isFalse()
+        val version = readVersionFromFile()
+        assertThat(version).isEqualTo(appLastUpdateTime)
+    }
+
+    @Test
+    fun dexFolderFor_returnPathToSdkDexFolder() {
+        val sdkFolderProvider = LocalSdkFolderProvider.create(context)
+        val dexFolder = sdkFolderProvider.dexFolderFor(
+            LocalSdkConfig(
+                packageName = "com.test.sdk.package",
+                dexPaths = listOf("1.dex", "2.dex"),
+                entryPoint = "compat.sdk.provider",
+            )
+        )
+        assertThat(dexFolder.exists()).isTrue()
+        assertThat(dexFolder).isEqualTo(
+            File(sdkRootFolder, "com.test.sdk.package")
+        )
+    }
+
+    @Test
+    fun dexFolderFor_doNotRemoveExistingFiles() {
+        val sdkFolderProvider = LocalSdkFolderProvider.create(context)
+
+        val sdkConfig = LocalSdkConfig(
+            packageName = "com.test.sdk.package",
+            dexPaths = listOf("1.dex", "2.dex"),
+            entryPoint = "compat.sdk.provider",
+        )
+
+        val dexFolder = sdkFolderProvider.dexFolderFor(sdkConfig)
+
+        val fileToKeep = File(dexFolder, "testFile")
+        fileToKeep.createNewFile()
+
+        val dexFolder2 = sdkFolderProvider.dexFolderFor(sdkConfig)
+        assertThat(dexFolder).isEqualTo(dexFolder2)
+
+        assertThat(fileToKeep.exists()).isTrue()
+    }
+
+    private fun createFileToDeleteInSdkRootFolder(): File {
+        val folder = File(sdkRootFolder, "folder")
+        folder.mkdirs()
+        val file = File(folder, "file")
+        file.createNewFile()
+        file.setReadOnly()
+        assertThat(file.exists()).isTrue()
+        return file
+    }
+
+    private fun readVersionFromFile(): Long {
+        return versionFile.inputStream().use { inputStream ->
+            DataInputStream(inputStream).use { dataStream ->
+                dataStream.readLong()
+            }
+        }
+    }
+
+    private fun createVersionFile(versionWriter: (DataOutputStream) -> Unit) {
+        if (!sdkRootFolder.exists()) {
+            sdkRootFolder.mkdirs()
+        }
+        versionFile.createNewFile()
+        versionFile.outputStream().use { outputStream ->
+            DataOutputStream(outputStream).use { dataStream ->
+                versionWriter(dataStream)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/TestLocalSdkStorage.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/TestLocalSdkStorage.kt
new file mode 100644
index 0000000..22cf607
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/storage/TestLocalSdkStorage.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.storage
+
+import android.content.Context
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import java.io.File
+
+/**
+ * Extract SDK DEX files in [rootFolder] / <packageName>.
+ */
+internal class TestLocalSdkStorage(
+    private val context: Context,
+    private val rootFolder: File
+) : LocalSdkStorage {
+    override fun dexFilesFor(sdkConfig: LocalSdkConfig): LocalSdkDexFiles {
+        val outputFolder = File(rootFolder, sdkConfig.packageName)
+        outputFolder.deleteRecursively()
+        outputFolder.mkdirs()
+
+        val fileList = buildList {
+            for (index in sdkConfig.dexPaths.indices) {
+                val dexFile = File(outputFolder, "$index.dex")
+                dexFile.createNewFile()
+                context.assets.open(sdkConfig.dexPaths[index]).use { inputStream ->
+                    dexFile.outputStream().use { outputStream ->
+                        inputStream.copyTo(outputStream)
+                    }
+                }
+                dexFile.setReadOnly()
+                add(dexFile)
+            }
+        }
+
+        return LocalSdkDexFiles(fileList)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
new file mode 100644
index 0000000..5d426e7
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.privacysandbox.sdkruntime.client
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.LoadSdkException
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SdkSandboxManager
+import android.content.Context
+import android.os.Bundle
+import android.os.ext.SdkExtensions.AD_SERVICES
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.core.os.asOutcomeReceiver
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfigsHolder
+import androidx.privacysandbox.sdkruntime.client.loader.LocalSdk
+import androidx.privacysandbox.sdkruntime.client.loader.SdkLoader
+import androidx.privacysandbox.sdkruntime.core.AdServicesInfo
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_ALREADY_LOADED
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.LOAD_SDK_NOT_FOUND
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.toLoadCompatSdkException
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import java.util.WeakHashMap
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Compat version of [SdkSandboxManager].
+ *
+ * Provides APIs to load [androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat]
+ * into SDK sandbox process or locally, and then interact with them.
+ *
+ * SdkSandbox process is a java process running in a separate uid range. Each app has its own
+ * SDK sandbox process.
+ *
+ * First app needs to declare SDKs it depends on in it's AndroidManifest.xml
+ * using <uses-sdk-library> tag. App can only load SDKs it depends on into the
+ * SDK sandbox process.
+ *
+ * For loading SDKs locally App need to bundle and declare local SDKs in
+ * assets/RuntimeEnabledSdkTable.xml with following format:
+ *
+ * <runtime-enabled-sdk-table>
+ *     <runtime-enabled-sdk>
+ *         <package-name>com.sdk1</package-name>
+ *         <compat-config-path>assets/RuntimeEnabledSdk-com.sdk1/CompatSdkConfig.xml</compat-config-path>
+ *     </runtime-enabled-sdk>
+ *     <runtime-enabled-sdk>
+ *         <package-name>com.sdk2</package-name>
+ *         <compat-config-path>assets/RuntimeEnabledSdk-com.sdk2/CompatSdkConfig.xml</compat-config-path>
+ *     </runtime-enabled-sdk>
+ * </runtime-enabled-sdk-table>
+ *
+ * Each local SDK should have config with following format:
+ *
+ * <compat-config>
+ *     <dex-path>RuntimeEnabledSdk-sdk.package.name/dex/classes.dex</dex-path>
+ *     <dex-path>RuntimeEnabledSdk-sdk.package.name/dex/classes2.dex</dex-path>
+ *     <java-resources-root-path>RuntimeEnabledSdk-sdk.package.name/res</java-resources-root-path>
+ *     <compat-entrypoint>com.sdk.EntryPointClass</compat-entrypoint>
+ *     <resource-id-remapping>
+ *         <r-package-class>com.test.sdk.RPackage</r-package-class>
+ *         <resources-package-id>123</resources-package-id>
+ *     </resource-id-remapping>
+ * </compat-config>
+ *
+ * @see [SdkSandboxManager]
+ */
+class SdkSandboxManagerCompat private constructor(
+    private val platformApi: PlatformApi,
+    private val configHolder: LocalSdkConfigsHolder,
+    private val sdkLoader: SdkLoader
+) {
+
+    private val localLoadedSdks = HashMap<String, LocalSdk>()
+
+    /**
+     * Load SDK in a SDK sandbox java process or locally.
+     *
+     * App should already declare SDKs it depends on in its AndroidManifest using
+     * <use-sdk-library> tag. App can only load SDKs it depends on into the SDK Sandbox process.
+     *
+     * When client application loads the first SDK, a new SdkSandbox process will be
+     * created, otherwise other SDKs will be loaded into the same sandbox which already created for
+     * the client application.
+     *
+     * Alternatively App could bundle and declare local SDKs dependencies in
+     * assets/RuntimeEnabledSdkTable.xml to load SDKs locally.
+     *
+     * This API may only be called while the caller is running in the foreground. Calls from the
+     * background will result in a [LoadSdkCompatException] being thrown.
+     *
+     * @param sdkName name of the SDK to be loaded.
+     * @param params additional parameters to be passed to the SDK in the form of a [Bundle]
+     *  as agreed between the client and the SDK.
+     * @return [SandboxedSdkCompat] from SDK on a successful run.
+     * @throws [LoadSdkCompatException] on fail.
+     *
+     * @see [SdkSandboxManager.loadSdk]
+     */
+    @Throws(LoadSdkCompatException::class)
+    suspend fun loadSdk(
+        sdkName: String,
+        params: Bundle
+    ): SandboxedSdkCompat {
+        if (localLoadedSdks.containsKey(sdkName)) {
+            throw LoadSdkCompatException(LOAD_SDK_ALREADY_LOADED, "$sdkName already loaded")
+        }
+
+        val sdkConfig = configHolder.getSdkConfig(sdkName)
+        if (sdkConfig != null) {
+            val sdkHolder = sdkLoader.loadSdk(sdkConfig)
+            val sandboxedSdkCompat = sdkHolder.onLoadSdk(params)
+            localLoadedSdks.put(sdkName, sdkHolder)
+            return sandboxedSdkCompat
+        }
+
+        return platformApi.loadSdk(sdkName, params)
+    }
+
+    private interface PlatformApi {
+        @DoNotInline
+        suspend fun loadSdk(sdkName: String, params: Bundle): SandboxedSdkCompat
+    }
+
+    // TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = AD_SERVICES, version = 4)
+    private class ApiAdServicesV4Impl(context: Context) : PlatformApi {
+        private val sdkSandboxManager = context.getSystemService(
+            SdkSandboxManager::class.java
+        )
+
+        @DoNotInline
+        override suspend fun loadSdk(
+            sdkName: String,
+            params: Bundle
+        ): SandboxedSdkCompat {
+            try {
+                val sandboxedSdk = loadSdkInternal(sdkName, params)
+                return SandboxedSdkCompat(sandboxedSdk)
+            } catch (ex: LoadSdkException) {
+                throw toLoadCompatSdkException(ex)
+            }
+        }
+
+        private suspend fun loadSdkInternal(
+            sdkName: String,
+            params: Bundle
+        ): SandboxedSdk {
+            return suspendCancellableCoroutine { continuation ->
+                sdkSandboxManager.loadSdk(
+                    sdkName,
+                    params,
+                    Runnable::run,
+                    continuation.asOutcomeReceiver()
+                )
+            }
+        }
+    }
+
+    private class FailImpl : PlatformApi {
+        @DoNotInline
+        override suspend fun loadSdk(
+            sdkName: String,
+            params: Bundle
+        ): SandboxedSdkCompat {
+            throw LoadSdkCompatException(LOAD_SDK_NOT_FOUND, "$sdkName not bundled with app")
+        }
+    }
+
+    companion object {
+
+        private val sInstances = WeakHashMap<Context, SdkSandboxManagerCompat>()
+
+        /**
+         *  Creates [SdkSandboxManagerCompat].
+         *
+         *  @param context Application context
+         *
+         *  @return SdkSandboxManagerCompat object.
+         */
+        @JvmStatic
+        fun from(context: Context): SdkSandboxManagerCompat {
+            synchronized(sInstances) {
+                var instance = sInstances[context]
+                if (instance == null) {
+                    val configHolder = LocalSdkConfigsHolder.load(context)
+                    val sdkLoader = SdkLoader.create(context)
+                    val platformApi =
+                        if (AdServicesInfo.version() >= 4) {
+                            ApiAdServicesV4Impl(context)
+                        } else {
+                            FailImpl()
+                        }
+                    instance = SdkSandboxManagerCompat(platformApi, configHolder, sdkLoader)
+                    sInstances[context] = instance
+                }
+                return instance
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/androidx/privacysandbox/sdkruntime/androidx-privacysandbox-sdkruntime-sdkruntime-client-documentation.md b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/androidx-privacysandbox-sdkruntime-sdkruntime-client-documentation.md
similarity index 100%
rename from privacysandbox/sdkruntime/sdkruntime-client/src/main/androidx/privacysandbox/sdkruntime/androidx-privacysandbox-sdkruntime-sdkruntime-client-documentation.md
rename to privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/androidx-privacysandbox-sdkruntime-sdkruntime-client-documentation.md
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfig.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfig.kt
new file mode 100644
index 0000000..081acf5
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfig.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+/**
+ * Information required for loading SDK bundled with App.
+ *
+ */
+internal data class LocalSdkConfig(
+    val packageName: String,
+    val dexPaths: List<String>,
+    val entryPoint: String,
+    val javaResourcesRoot: String? = null,
+    val resourceRemapping: ResourceRemappingConfig? = null
+)
+
+internal data class ResourceRemappingConfig(
+    val rPackageClassName: String,
+    val packageId: Int
+)
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt
new file mode 100644
index 0000000..b9f10c2
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigParser.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+import android.util.Xml
+import java.io.InputStream
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParser.END_TAG
+import org.xmlpull.v1.XmlPullParser.START_TAG
+import org.xmlpull.v1.XmlPullParserException
+
+/**
+ * Parser for SDK config.
+ *
+ * The expected XML structure is:
+ * <compat-config>
+ *     <dex-path>RuntimeEnabledSdk-sdk.package.name/dex/classes.dex</dex-path>
+ *     <dex-path>RuntimeEnabledSdk-sdk.package.name/dex/classes2.dex</dex-path>
+ *     <java-resources-root-path>RuntimeEnabledSdk-sdk.package.name/res</java-resources-root-path>
+ *     <compat-entrypoint>com.sdk.EntryPointClass</compat-entrypoint>
+ *     <resource-id-remapping>
+ *         <r-package-class>com.test.sdk.RPackage</r-package-class>
+ *         <resources-package-id>123</resources-package-id>
+ *     </resource-id-remapping>
+ * </compat-config>
+ */
+internal class LocalSdkConfigParser private constructor(
+    private val xmlParser: XmlPullParser
+) {
+
+    private fun readConfig(packageName: String): LocalSdkConfig {
+        xmlParser.require(XmlPullParser.START_DOCUMENT, NAMESPACE, null)
+        xmlParser.nextTag()
+
+        val dexPaths = mutableListOf<String>()
+        var javaResourcesRoot: String? = null
+        var entryPoint: String? = null
+        var resourceRemapping: ResourceRemappingConfig? = null
+
+        xmlParser.require(START_TAG, NAMESPACE, CONFIG_ELEMENT_NAME)
+        while (xmlParser.next() != END_TAG) {
+            if (xmlParser.eventType != START_TAG) {
+                continue
+            }
+            when (xmlParser.name) {
+                DEX_PATH_ELEMENT_NAME -> {
+                    val dexPath = xmlParser.nextText()
+                    dexPaths.add(dexPath)
+                }
+
+                RESOURCE_ROOT_ELEMENT_NAME -> {
+                    if (javaResourcesRoot != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $RESOURCE_ROOT_ELEMENT_NAME tag found"
+                        )
+                    }
+                    javaResourcesRoot = xmlParser.nextText()
+                }
+
+                ENTRYPOINT_ELEMENT_NAME -> {
+                    if (entryPoint != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $ENTRYPOINT_ELEMENT_NAME tag found"
+                        )
+                    }
+                    entryPoint = xmlParser.nextText()
+                }
+
+                RESOURCE_REMAPPING_ENTRY_ELEMENT_NAME -> {
+                    if (resourceRemapping != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $RESOURCE_REMAPPING_ENTRY_ELEMENT_NAME tag found"
+                        )
+                    }
+                    resourceRemapping = readResourceRemappingConfig()
+                }
+
+                else -> xmlParser.skipCurrentTag()
+            }
+        }
+        xmlParser.require(END_TAG, NAMESPACE, CONFIG_ELEMENT_NAME)
+
+        if (entryPoint == null) {
+            throw XmlPullParserException("No $ENTRYPOINT_ELEMENT_NAME tag found")
+        }
+        if (dexPaths.isEmpty()) {
+            throw XmlPullParserException("No $DEX_PATH_ELEMENT_NAME tags found")
+        }
+
+        return LocalSdkConfig(
+            packageName,
+            dexPaths,
+            entryPoint,
+            javaResourcesRoot,
+            resourceRemapping
+        )
+    }
+
+    private fun readResourceRemappingConfig(): ResourceRemappingConfig {
+        var rPackageClassName: String? = null
+        var packageId: Int? = null
+
+        xmlParser.require(START_TAG, NAMESPACE, RESOURCE_REMAPPING_ENTRY_ELEMENT_NAME)
+        while (xmlParser.next() != END_TAG) {
+            if (xmlParser.eventType != START_TAG) {
+                continue
+            }
+            when (xmlParser.name) {
+                RESOURCE_REMAPPING_CLASS_ELEMENT_NAME -> {
+                    if (rPackageClassName != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $RESOURCE_REMAPPING_CLASS_ELEMENT_NAME tag found"
+                        )
+                    }
+                    rPackageClassName = xmlParser.nextText()
+                }
+
+                RESOURCE_REMAPPING_ID_ELEMENT_NAME -> {
+                    if (packageId != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $RESOURCE_REMAPPING_ID_ELEMENT_NAME tag found"
+                        )
+                    }
+                    packageId = xmlParser.nextText().toInt()
+                }
+
+                else -> xmlParser.skipCurrentTag()
+            }
+        }
+        xmlParser.require(END_TAG, NAMESPACE, RESOURCE_REMAPPING_ENTRY_ELEMENT_NAME)
+
+        if (rPackageClassName == null) {
+            throw XmlPullParserException(
+                "No $RESOURCE_REMAPPING_CLASS_ELEMENT_NAME tag found"
+            )
+        }
+        if (packageId == null) {
+            throw XmlPullParserException(
+                "No $RESOURCE_REMAPPING_ID_ELEMENT_NAME tag found"
+            )
+        }
+
+        return ResourceRemappingConfig(rPackageClassName, packageId)
+    }
+
+    companion object {
+        private val NAMESPACE: String? = null // We don't use namespaces
+        private const val CONFIG_ELEMENT_NAME = "compat-config"
+        private const val DEX_PATH_ELEMENT_NAME = "dex-path"
+        private const val RESOURCE_ROOT_ELEMENT_NAME = "java-resources-root-path"
+        private const val ENTRYPOINT_ELEMENT_NAME = "compat-entrypoint"
+        private const val RESOURCE_REMAPPING_ENTRY_ELEMENT_NAME = "resource-id-remapping"
+        private const val RESOURCE_REMAPPING_CLASS_ELEMENT_NAME = "r-package-class"
+        private const val RESOURCE_REMAPPING_ID_ELEMENT_NAME = "resources-package-id"
+
+        fun parse(inputStream: InputStream, packageName: String): LocalSdkConfig {
+            val parser = Xml.newPullParser()
+            try {
+                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
+                parser.setInput(inputStream, null)
+                return LocalSdkConfigParser(parser).readConfig(packageName)
+            } finally {
+                parser.setInput(null)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolder.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolder.kt
new file mode 100644
index 0000000..4232878
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolder.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import java.io.FileNotFoundException
+
+/**
+ * Holds information about all SDKs bundled with App.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class LocalSdkConfigsHolder private constructor(
+    private val configs: Map<String, LocalSdkConfig>
+) {
+
+    fun getSdkConfig(sdkName: String): LocalSdkConfig? {
+        return configs[sdkName]
+    }
+
+    companion object {
+        private const val SDK_TABLE_ASSET_NAME = "RuntimeEnabledSdkTable.xml"
+
+        fun load(
+            context: Context,
+            sdkTableAssetName: String = SDK_TABLE_ASSET_NAME
+        ): LocalSdkConfigsHolder {
+            val sdkTable = loadSdkTable(context, sdkTableAssetName)
+
+            val data = buildMap {
+                for ((packageName, configPath) in sdkTable) {
+                    context.assets.open(configPath).use { sdkConfigAsset ->
+                        val sdkInfo = LocalSdkConfigParser.parse(sdkConfigAsset, packageName)
+                        put(packageName, sdkInfo)
+                    }
+                }
+            }
+
+            return LocalSdkConfigsHolder(data)
+        }
+
+        private fun loadSdkTable(
+            context: Context,
+            sdkTableAssetName: String
+        ): Set<SdkTableConfigParser.SdkTableEntry> {
+            try {
+                context.assets.open(sdkTableAssetName).use { sdkTableAsset ->
+                    return SdkTableConfigParser.parse(sdkTableAsset)
+                }
+            } catch (ignored: FileNotFoundException) {
+                return emptySet()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/SdkTableConfigParser.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/SdkTableConfigParser.kt
new file mode 100644
index 0000000..eea19cc
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/SdkTableConfigParser.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.config
+
+import android.util.Xml
+import androidx.annotation.RestrictTo
+import java.io.InputStream
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParser.END_TAG
+import org.xmlpull.v1.XmlPullParser.START_TAG
+import org.xmlpull.v1.XmlPullParserException
+
+/**
+ * Parser for config with paths to compat SDK configs for each SDK that bundled with app.
+ *
+ * The expected XML structure is:
+ * <runtime-enabled-sdk-table>
+ *     <runtime-enabled-sdk>
+ *         <package-name>com.sdk1</package-name>
+ *         <compat-config-path>assets/RuntimeEnabledSdk-com.sdk1/CompatSdkConfig.xml</compat-config-path>
+ *     </runtime-enabled-sdk>
+ *     <runtime-enabled-sdk>
+ *         <package-name>com.sdk2</package-name>
+ *         <compat-config-path>assets/RuntimeEnabledSdk-com.sdk2/CompatSdkConfig.xml</compat-config-path>
+ *     </runtime-enabled-sdk>
+ * </runtime-enabled-sdk-table>
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class SdkTableConfigParser private constructor(
+    private val xmlParser: XmlPullParser
+) {
+
+    private fun readSdkTable(): Set<SdkTableEntry> {
+        xmlParser.require(XmlPullParser.START_DOCUMENT, NAMESPACE, null)
+        xmlParser.nextTag()
+
+        val packages = mutableSetOf<String>()
+
+        return buildSet {
+            xmlParser.require(START_TAG, NAMESPACE, SDK_TABLE_ELEMENT_NAME)
+            while (xmlParser.next() != END_TAG) {
+                if (xmlParser.eventType != START_TAG) {
+                    continue
+                }
+                if (xmlParser.name == SDK_ENTRY_ELEMENT_NAME) {
+                    val entry = readSdkEntry()
+                    if (!packages.add(entry.packageName)) {
+                        throw XmlPullParserException(
+                            "Duplicate entry for ${entry.packageName} found"
+                        )
+                    }
+                    add(entry)
+                } else {
+                    xmlParser.skipCurrentTag()
+                }
+            }
+            xmlParser.require(END_TAG, NAMESPACE, SDK_TABLE_ELEMENT_NAME)
+        }
+    }
+
+    private fun readSdkEntry(): SdkTableEntry {
+        var packageName: String? = null
+        var configPath: String? = null
+
+        xmlParser.require(START_TAG, NAMESPACE, SDK_ENTRY_ELEMENT_NAME)
+        while (xmlParser.next() != END_TAG) {
+            if (xmlParser.eventType != START_TAG) {
+                continue
+            }
+            when (xmlParser.name) {
+                SDK_PACKAGE_NAME_ELEMENT_NAME -> {
+                    if (packageName != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $SDK_PACKAGE_NAME_ELEMENT_NAME tag found"
+                        )
+                    }
+                    packageName = xmlParser.nextText()
+                }
+
+                COMPAT_CONFIG_PATH_ELEMENT_NAME -> {
+                    if (configPath != null) {
+                        throw XmlPullParserException(
+                            "Duplicate $COMPAT_CONFIG_PATH_ELEMENT_NAME tag found"
+                        )
+                    }
+                    configPath = xmlParser.nextText()
+                }
+
+                else -> xmlParser.skipCurrentTag()
+            }
+        }
+        xmlParser.require(END_TAG, NAMESPACE, SDK_ENTRY_ELEMENT_NAME)
+
+        if (packageName == null) {
+            throw XmlPullParserException(
+                "No $SDK_PACKAGE_NAME_ELEMENT_NAME tag found"
+            )
+        }
+        if (configPath == null) {
+            throw XmlPullParserException(
+                "No $COMPAT_CONFIG_PATH_ELEMENT_NAME tag found"
+            )
+        }
+
+        return SdkTableEntry(packageName, configPath)
+    }
+
+    internal data class SdkTableEntry(
+        val packageName: String,
+        val compatConfigPath: String,
+    )
+
+    companion object {
+        private val NAMESPACE: String? = null // We don't use namespaces
+        private const val SDK_TABLE_ELEMENT_NAME = "runtime-enabled-sdk-table"
+        private const val SDK_ENTRY_ELEMENT_NAME = "runtime-enabled-sdk"
+        private const val SDK_PACKAGE_NAME_ELEMENT_NAME = "package-name"
+        private const val COMPAT_CONFIG_PATH_ELEMENT_NAME = "compat-config-path"
+
+        fun parse(inputStream: InputStream): Set<SdkTableEntry> {
+            val parser = Xml.newPullParser()
+            try {
+                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
+                parser.setInput(inputStream, null)
+                return SdkTableConfigParser(parser).readSdkTable()
+            } finally {
+                parser.setInput(null)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/XmlUtils.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/XmlUtils.kt
new file mode 100644
index 0000000..6a91815
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/config/XmlUtils.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+
+package androidx.privacysandbox.sdkruntime.client.config
+
+import androidx.annotation.RestrictTo
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParser.END_TAG
+import org.xmlpull.v1.XmlPullParser.START_TAG
+
+/**
+ * Skip current tag (including inner tags)
+ *
+ * @suppress
+ */
+internal fun XmlPullParser.skipCurrentTag() {
+    require(START_TAG, null, null)
+    var depth = 1
+    while (depth != 0) {
+        when (next()) {
+            END_TAG -> depth--
+            START_TAG -> depth++
+        }
+    }
+}
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/FileClassLoaderFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/FileClassLoaderFactory.kt
new file mode 100644
index 0000000..e6d8eb6
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/FileClassLoaderFactory.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.util.Log
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.client.loader.storage.LocalSdkStorage
+import androidx.privacysandbox.sdkruntime.client.loader.storage.toClassPathString
+import dalvik.system.BaseDexClassLoader
+import java.io.File
+
+/**
+ * Loading SDK using BaseDexClassLoader.
+ * Using [LocalSdkStorage] to get SDK DEX files,
+ * if no files available - delegating to fallback.
+ */
+internal class FileClassLoaderFactory(
+    private val localSdkStorage: LocalSdkStorage,
+    private val fallback: SdkLoader.ClassLoaderFactory,
+) : SdkLoader.ClassLoaderFactory {
+
+    override fun createClassLoaderFor(
+        sdkConfig: LocalSdkConfig,
+        parent: ClassLoader
+    ): ClassLoader {
+        return tryCreateBaseDexClassLoaderFor(sdkConfig, parent)
+            ?: fallback.createClassLoaderFor(
+                sdkConfig,
+                parent
+            )
+    }
+
+    private fun tryCreateBaseDexClassLoaderFor(
+        sdkConfig: LocalSdkConfig,
+        parent: ClassLoader
+    ): ClassLoader? {
+        try {
+            val dexFiles = localSdkStorage.dexFilesFor(sdkConfig)
+            if (dexFiles == null) {
+                Log.w(
+                    LOG_TAG,
+                    "Can't use BaseDexClassLoader for ${sdkConfig.packageName} - no dexFiles"
+                )
+                return null
+            }
+
+            val optimizedDirectory = File(dexFiles.files[0].parentFile, "DexOpt")
+            if (!optimizedDirectory.exists()) {
+                optimizedDirectory.mkdirs()
+            }
+
+            return BaseDexClassLoader(
+                dexFiles.toClassPathString(),
+                optimizedDirectory,
+                /* librarySearchPath = */ null,
+                parent
+            )
+        } catch (ex: Exception) {
+            Log.e(
+                LOG_TAG,
+                "Failed to use BaseDexClassLoader for ${sdkConfig.packageName} - exception",
+                ex
+            )
+            return null
+        }
+    }
+
+    companion object {
+        const val LOG_TAG =
+            "FileClassLoaderFactory"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/InMemorySdkClassLoaderFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/InMemorySdkClassLoaderFactory.kt
new file mode 100644
index 0000000..2fa4a55
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/InMemorySdkClassLoaderFactory.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import android.content.res.AssetManager
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import dalvik.system.InMemoryDexClassLoader
+import java.nio.ByteBuffer
+import java.nio.channels.Channels
+
+/**
+ * Loading SDK in memory on API 27+
+ */
+internal abstract class InMemorySdkClassLoaderFactory : SdkLoader.ClassLoaderFactory {
+
+    @RequiresApi(Build.VERSION_CODES.O_MR1)
+    internal class InMemoryImpl(
+        private val assetManager: AssetManager
+    ) : InMemorySdkClassLoaderFactory() {
+
+        @DoNotInline
+        override fun createClassLoaderFor(
+            sdkConfig: LocalSdkConfig,
+            parent: ClassLoader
+        ): ClassLoader {
+            try {
+                val buffers = arrayOfNulls<ByteBuffer>(sdkConfig.dexPaths.size)
+                for (i in sdkConfig.dexPaths.indices) {
+                    assetManager.open(sdkConfig.dexPaths[i]).use { inputStream ->
+                        val byteBuffer = ByteBuffer.allocate(inputStream.available())
+                        Channels.newChannel(inputStream).read(byteBuffer)
+                        byteBuffer.flip()
+                        buffers[i] = byteBuffer
+                    }
+                }
+                return InMemoryDexClassLoader(buffers, parent)
+            } catch (ex: Exception) {
+                throw LoadSdkCompatException(
+                    LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
+                    "Failed to instantiate classloader",
+                    ex
+                )
+            }
+        }
+    }
+
+    internal class FailImpl : InMemorySdkClassLoaderFactory() {
+        @DoNotInline
+        override fun createClassLoaderFor(
+            sdkConfig: LocalSdkConfig,
+            parent: ClassLoader
+        ): ClassLoader {
+            throw LoadSdkCompatException(
+                LoadSdkCompatException.LOAD_SDK_SDK_SANDBOX_DISABLED,
+                "Can't use InMemoryDexClassLoader"
+            )
+        }
+    }
+
+    companion object {
+        fun create(context: Context): InMemorySdkClassLoaderFactory {
+            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+                InMemoryImpl(context.assets)
+            } else {
+                FailImpl()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactory.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactory.kt
new file mode 100644
index 0000000..b669af8
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/JavaResourcesLoadingClassLoaderFactory.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import java.io.File
+import java.io.IOException
+import java.net.URL
+import java.util.Enumeration
+
+/**
+ * Delegate java resources related calls to app classloader.
+ *
+ * Classloaders normally delegate calls to parent classloader first, that's why this factory
+ * creates classloader that will work with java resources and pass it as parent to
+ * [codeClassLoaderFactory] thus overrides java resources for all classes loaded down the line.
+ *
+ * Add [LocalSdkConfig.javaResourcesRoot] as prefix to resource names before delegating calls,
+ * thus allowing isolating java resources for different local sdks.
+ */
+internal class JavaResourcesLoadingClassLoaderFactory(
+    private val appClassloader: ClassLoader,
+    private val codeClassLoaderFactory: SdkLoader.ClassLoaderFactory
+) : SdkLoader.ClassLoaderFactory {
+    override fun createClassLoaderFor(
+        sdkConfig: LocalSdkConfig,
+        parent: ClassLoader
+    ): ClassLoader {
+        val javaResourcesLoadingClassLoader = createJavaResourcesLoadingClassLoader(
+            sdkConfig,
+            parent
+        )
+        return codeClassLoaderFactory.createClassLoaderFor(
+            sdkConfig,
+            parent = javaResourcesLoadingClassLoader
+        )
+    }
+
+    private fun createJavaResourcesLoadingClassLoader(
+        sdkConfig: LocalSdkConfig,
+        parent: ClassLoader
+    ): ClassLoader {
+        return if (sdkConfig.javaResourcesRoot == null) {
+            parent
+        } else {
+            JavaResourcesLoadingClassLoader(
+                parent,
+                appClassloader,
+                File(ASSETS_DIR, sdkConfig.javaResourcesRoot)
+            )
+        }
+    }
+
+    private class JavaResourcesLoadingClassLoader constructor(
+        parent: ClassLoader,
+        private val appClassloader: ClassLoader,
+        private val javaResourcePrefix: File
+    ) : ClassLoader(parent) {
+        override fun findResource(name: String): URL? {
+            return appClassloader.getResource(File(javaResourcePrefix, name).path)
+        }
+
+        @Throws(IOException::class)
+        override fun findResources(name: String): Enumeration<URL> {
+            return appClassloader.getResources(File(javaResourcePrefix, name).path)
+        }
+    }
+
+    companion object {
+        const val ASSETS_DIR = "assets/"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdk.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdk.kt
new file mode 100644
index 0000000..0867e6e
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/LocalSdk.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.os.Bundle
+import androidx.annotation.RestrictTo
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import org.jetbrains.annotations.TestOnly
+
+/**
+ * Provides interface for interaction with locally loaded SDK.
+ * Handle different protocol versions inside.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal abstract class LocalSdk protected constructor(
+    @get:TestOnly val sdkProvider: Any
+) {
+
+    abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+
+    abstract fun beforeUnloadSdk()
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/ResourceRemapping.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/ResourceRemapping.kt
new file mode 100644
index 0000000..711d7b1
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/ResourceRemapping.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.config.ResourceRemappingConfig
+
+/**
+ * Update RPackage.packageId for supporting Android Resource remapping for SDK.
+ * Each resource has id calculated as id = RPackage.packageId + index.
+ * Updating packageId effectively shifting all SDK resource ids in resource table.
+ * IMPORTANT: ResourceRemapping should happen before ANY interactions with R.class
+ */
+internal object ResourceRemapping {
+
+    private const val PACKAGE_ID_FIELD_NAME = "packageId"
+
+    fun apply(
+        sdkClassLoader: ClassLoader,
+        remappingConfig: ResourceRemappingConfig?
+    ) {
+        if (remappingConfig == null)
+            return
+
+        val rPackageClass = Class.forName(
+            remappingConfig.rPackageClassName,
+            /* initialize = */ false,
+            sdkClassLoader
+        )
+
+        val field = rPackageClass.getDeclaredField(PACKAGE_ID_FIELD_NAME)
+
+        field.setInt(null, remappingConfig.packageId)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
new file mode 100644
index 0000000..89e3457
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoader.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.content.Context
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import androidx.privacysandbox.sdkruntime.client.loader.impl.SdkV1
+import androidx.privacysandbox.sdkruntime.client.loader.storage.CachedLocalSdkStorage
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+
+/**
+ * Load SDK bundled with App.
+ */
+internal class SdkLoader internal constructor(
+    private val classLoaderFactory: ClassLoaderFactory,
+    private val appContext: Context
+) {
+
+    internal interface ClassLoaderFactory {
+        fun createClassLoaderFor(sdkConfig: LocalSdkConfig, parent: ClassLoader): ClassLoader
+    }
+
+    /**
+     * Loading SDK in separate classloader:
+     *  1. Create classloader for sdk;
+     *  2. Performing handshake to determine api version;
+     *  3. (optional) Update RPackage.packageId to support Android Resource remapping for SDK
+     *  4. Select [LocalSdk] implementation that could work with that api version.
+     *
+     * @param sdkConfig sdk to load
+     * @return LocalSdk implementation for loaded SDK
+     */
+    fun loadSdk(sdkConfig: LocalSdkConfig): LocalSdk {
+        val classLoader = classLoaderFactory.createClassLoaderFor(
+            sdkConfig,
+            getParentClassLoader()
+        )
+        return createLocalSdk(classLoader, sdkConfig)
+    }
+
+    private fun getParentClassLoader(): ClassLoader = appContext.classLoader.parent!!
+
+    private fun createLocalSdk(classLoader: ClassLoader, sdkConfig: LocalSdkConfig): LocalSdk {
+        try {
+            val apiVersion = VersionHandshake.perform(classLoader)
+            ResourceRemapping.apply(classLoader, sdkConfig.resourceRemapping)
+            if (apiVersion >= 1) {
+                return SdkV1.create(classLoader, sdkConfig.entryPoint, appContext)
+            }
+        } catch (ex: Exception) {
+            throw LoadSdkCompatException(
+                LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
+                "Failed to instantiate local SDK",
+                ex
+            )
+        }
+
+        throw LoadSdkCompatException(
+            LoadSdkCompatException.LOAD_SDK_NOT_FOUND,
+            "Incorrect Api version"
+        )
+    }
+
+    companion object {
+        /**
+         * Build chain of [ClassLoaderFactory] that could load SDKs with their resources.
+         * Order is important because classloaders normally delegate calls to parent classloader
+         * first:
+         *  1. [JavaResourcesLoadingClassLoaderFactory] - to provide java resources to classes
+         *  loaded by child classloaders;
+         *  2a. [FileClassLoaderFactory] - first trying to use factory that extracting SDK Dex
+         *  to storage and load it using [dalvik.system.BaseDexClassLoader].
+         *  Supports all platform versions (Api14+, minSdkVersion for library).
+         *  2b. [InMemorySdkClassLoaderFactory] - fallback for low available space. Trying to load
+         *  SDK in-memory using [dalvik.system.InMemoryDexClassLoader].
+         *  Supports Api27+ only, fails SDK loading on non-supported platform versions.
+         *
+         * @param context App context
+         * @param lowSpaceThreshold Minimal available space in bytes required to proceed with
+         * extracting SDK Dex files.
+         *
+         * @return SdkLoader that could load SDKs with their resources.
+         */
+        fun create(
+            context: Context,
+            lowSpaceThreshold: Long = 100 * 1024 * 1024
+        ): SdkLoader {
+            val cachedLocalSdkStorage = CachedLocalSdkStorage.create(
+                context,
+                lowSpaceThreshold
+            )
+            val classLoaderFactory = JavaResourcesLoadingClassLoaderFactory(
+                context.classLoader,
+                codeClassLoaderFactory = FileClassLoaderFactory(
+                    cachedLocalSdkStorage,
+                    fallback = InMemorySdkClassLoaderFactory.create(context)
+                )
+            )
+            return SdkLoader(classLoaderFactory, context)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/VersionHandshake.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/VersionHandshake.kt
new file mode 100644
index 0000000..21a6a17
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/VersionHandshake.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import android.annotation.SuppressLint
+import androidx.annotation.RestrictTo
+import androidx.privacysandbox.sdkruntime.core.Versions
+
+/**
+ * Performing version handshake.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal object VersionHandshake {
+    @SuppressLint("BanUncheckedReflection") // calling method on Versions class
+    fun perform(classLoader: ClassLoader?): Int {
+        val versionsClass = Class.forName(
+            Versions::class.java.name,
+            false,
+            classLoader
+        )
+        val handShakeMethod = versionsClass.getMethod("handShake", Int::class.javaPrimitiveType)
+        return handShakeMethod.invoke(null, Versions.API_VERSION) as Int
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt
new file mode 100644
index 0000000..4a9fd3b
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SandboxedSdkContextCompat.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.impl
+
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.annotation.RestrictTo
+
+/**
+ * Refers to the context of the SDK loaded locally.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class SandboxedSdkContextCompat(
+    base: Context,
+    private val classLoader: ClassLoader?
+) : ContextWrapper(base) {
+    override fun getClassLoader(): ClassLoader? {
+        return classLoader
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt
new file mode 100644
index 0000000..9602b3cd
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/impl/SdkV1.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.impl
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Bundle
+import android.os.IBinder
+import androidx.annotation.RestrictTo
+import androidx.privacysandbox.sdkruntime.client.loader.LocalSdk
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+
+/**
+ * Provides interface for interaction with locally loaded SDK with ApiVersion 1.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class SdkV1 private constructor(
+    sdkProvider: Any,
+
+    private val onLoadSdkMethod: Method,
+    private val beforeUnloadSdkMethod: Method,
+
+    private val sandboxedSdkCompatBuilder: SandboxedSdkCompatBuilderV1,
+    private val loadSdkCompatExceptionBuilder: LoadSdkCompatExceptionBuilderV1
+) : LocalSdk(sdkProvider) {
+
+    @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+    override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+        try {
+            val rawResult = onLoadSdkMethod.invoke(sdkProvider, params)
+            return sandboxedSdkCompatBuilder.build(rawResult!!)
+        } catch (e: InvocationTargetException) {
+            throw loadSdkCompatExceptionBuilder.tryRebuildCompatException(e.targetException)
+        } catch (ex: Exception) {
+            throw LoadSdkCompatException(
+                LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
+                "Failed during onLoadSdk call",
+                ex
+            )
+        }
+    }
+
+    @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+    override fun beforeUnloadSdk() {
+        beforeUnloadSdkMethod.invoke(sdkProvider)
+    }
+
+    internal class SandboxedSdkCompatBuilderV1 private constructor(
+        private val getInterfaceMethod: Method
+    ) {
+
+        @SuppressLint("BanUncheckedReflection") // calling method on SandboxedSdkCompat class
+        fun build(rawObject: Any): SandboxedSdkCompat {
+            val binder = getInterfaceMethod.invoke(rawObject) as IBinder
+            return SandboxedSdkCompat(binder)
+        }
+
+        companion object {
+
+            fun create(classLoader: ClassLoader?): SandboxedSdkCompatBuilderV1 {
+                val sandboxedSdkCompatClass = Class.forName(
+                    SandboxedSdkCompat::class.java.name,
+                    /* initialize = */ false,
+                    classLoader
+                )
+                val getInterfaceMethod = sandboxedSdkCompatClass.getMethod("getInterface")
+                return SandboxedSdkCompatBuilderV1(getInterfaceMethod)
+            }
+        }
+    }
+
+    internal class LoadSdkCompatExceptionBuilderV1 private constructor(
+        private val getLoadSdkErrorCodeMethod: Method,
+        private val getExtraInformationMethod: Method
+    ) {
+        @SuppressLint("BanUncheckedReflection") // calling method on LoadSdkCompatException class
+        fun tryRebuildCompatException(rawException: Throwable): Throwable {
+            if (rawException.javaClass.name != LoadSdkCompatException::class.java.name) {
+                return rawException
+            }
+
+            return try {
+                val loadSdkErrorCode = getLoadSdkErrorCodeMethod.invoke(rawException) as Int
+                val extraInformation = getExtraInformationMethod.invoke(rawException) as Bundle
+                LoadSdkCompatException(
+                    loadSdkErrorCode,
+                    rawException.message,
+                    rawException.cause,
+                    extraInformation
+                )
+            } catch (ex: Throwable) {
+                // failed to rebuild, just wrap original
+                LoadSdkCompatException(
+                    LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
+                    "Failed to rebuild exception with error ${ex.message}",
+                    rawException
+                )
+            }
+        }
+
+        companion object {
+            fun create(classLoader: ClassLoader?): LoadSdkCompatExceptionBuilderV1 {
+                val loadSdkCompatExceptionClass = Class.forName(
+                    LoadSdkCompatException::class.java.name,
+                    /* initialize = */ false,
+                    classLoader
+                )
+                val getLoadSdkErrorCodeMethod = loadSdkCompatExceptionClass.getMethod(
+                    "getLoadSdkErrorCode"
+                )
+                val getExtraInformationMethod = loadSdkCompatExceptionClass.getMethod(
+                    "getExtraInformation"
+                )
+                return LoadSdkCompatExceptionBuilderV1(
+                    getLoadSdkErrorCodeMethod,
+                    getExtraInformationMethod
+                )
+            }
+        }
+    }
+
+    companion object {
+
+        @SuppressLint("BanUncheckedReflection") // calling method of SandboxedSdkProviderCompat
+        fun create(
+            classLoader: ClassLoader?,
+            sdkProviderClassName: String,
+            appContext: Context
+        ): SdkV1 {
+            val sdkProviderClass = Class.forName(
+                sdkProviderClassName,
+                /* initialize = */ false,
+                classLoader
+            )
+            val attachContextMethod =
+                sdkProviderClass.getMethod("attachContext", Context::class.java)
+            val onLoadSdkMethod = sdkProviderClass.getMethod("onLoadSdk", Bundle::class.java)
+            val beforeUnloadSdkMethod = sdkProviderClass.getMethod("beforeUnloadSdk")
+            val sandboxedSdkCompatBuilder = SandboxedSdkCompatBuilderV1.create(classLoader)
+            val loadSdkCompatExceptionBuilder =
+                LoadSdkCompatExceptionBuilderV1.create(classLoader)
+
+            val sdkProvider = sdkProviderClass.getConstructor().newInstance()
+            val sandboxedSdkContextCompat = SandboxedSdkContextCompat(appContext, classLoader)
+            attachContextMethod.invoke(sdkProvider, sandboxedSdkContextCompat)
+
+            return SdkV1(
+                sdkProvider,
+                onLoadSdkMethod,
+                beforeUnloadSdkMethod,
+                sandboxedSdkCompatBuilder,
+                loadSdkCompatExceptionBuilder
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/CachedLocalSdkStorage.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/CachedLocalSdkStorage.kt
new file mode 100644
index 0000000..82251fc
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/CachedLocalSdkStorage.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.storage
+
+import android.content.Context
+import android.os.Build
+import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2
+import android.os.Environment
+import android.os.StatFs
+import android.util.Log
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import java.io.File
+
+/**
+ * Caching implementation of [LocalSdkStorage].
+ *
+ * Extract SDK DEX files into folder provided by [LocalSdkFolderProvider].
+ * Not extracting new files if available space lower than lowSpaceThreshold.
+ * Return result only if all files successfully extracted.
+ */
+internal class CachedLocalSdkStorage private constructor(
+    private val context: Context,
+    private val rootFolderProvider: LocalSdkFolderProvider,
+    private val lowSpaceThreshold: Long
+) : LocalSdkStorage {
+
+    /**
+     * Return SDK DEX files from folder provided by [LocalSdkFolderProvider].
+     * Extract missing files from assets if available space bigger than lowSpaceThreshold.
+     *
+     * @param sdkConfig sdk config
+     * @return [LocalSdkDexFiles] if all SDK DEX files available in SDK folder
+     * or null if something missing and couldn't be extracted because of
+     * available space lower than lowSpaceThreshold
+     */
+    override fun dexFilesFor(sdkConfig: LocalSdkConfig): LocalSdkDexFiles? {
+        val disableExtracting = availableBytes() < lowSpaceThreshold
+        val targetFolder = rootFolderProvider.dexFolderFor(sdkConfig)
+
+        try {
+            val files = buildList {
+                for (index in sdkConfig.dexPaths.indices) {
+                    val assetName = sdkConfig.dexPaths[index]
+                    val outputFileName = "$index.dex"
+                    val outputDexFile = File(targetFolder, outputFileName)
+
+                    if (!outputDexFile.exists()) {
+                        if (disableExtracting) {
+                            Log.i(LOG_TAG, "Can't extract $assetName because of low space")
+                            return null
+                        }
+                        extractAssetToFile(assetName, outputDexFile)
+                    }
+
+                    add(outputDexFile)
+                }
+            }
+            return LocalSdkDexFiles(files)
+        } catch (ex: Exception) {
+            Log.e(
+                LOG_TAG,
+                "Failed to extract ${sdkConfig.packageName}, deleting $targetFolder.",
+                ex
+            )
+
+            if (!targetFolder.deleteRecursively()) {
+                Log.e(
+                    LOG_TAG,
+                    "Failed to delete $targetFolder during cleanup.",
+                    ex
+                )
+            }
+
+            throw ex
+        }
+    }
+
+    private fun extractAssetToFile(
+        assetName: String,
+        outputFile: File,
+    ) {
+        outputFile.createNewFile()
+        context.assets.open(assetName).use { fromStream ->
+            outputFile.outputStream().use { toStream ->
+                fromStream.copyTo(toStream)
+            }
+        }
+        outputFile.setReadOnly()
+    }
+
+    private fun availableBytes(): Long {
+        val dataDirectory = Environment.getDataDirectory()
+        val statFs = StatFs(dataDirectory.path)
+        if (Build.VERSION.SDK_INT >= JELLY_BEAN_MR2) {
+            return Api18Impl.availableBytes(statFs)
+        }
+
+        @Suppress("DEPRECATION")
+        val blockSize = statFs.blockSize.toLong()
+
+        @Suppress("DEPRECATION")
+        val availableBlocks = statFs.availableBlocks.toLong()
+
+        return availableBlocks * blockSize
+    }
+
+    @RequiresApi(JELLY_BEAN_MR2)
+    private object Api18Impl {
+        @DoNotInline
+        fun availableBytes(statFs: StatFs) = statFs.availableBytes
+    }
+
+    companion object {
+
+        const val LOG_TAG = "CachedLocalSdkStorage"
+
+        /**
+         * Create CachedLocalSdkStorage.
+         *
+         * @param context Application context
+         * @param lowSpaceThreshold Minimal available space in bytes required to proceed with
+         * extracting new SDK Dex files.
+         */
+        fun create(
+            context: Context,
+            lowSpaceThreshold: Long
+        ): CachedLocalSdkStorage {
+            val localSdkFolderProvider = LocalSdkFolderProvider.create(context)
+            return CachedLocalSdkStorage(context, localSdkFolderProvider, lowSpaceThreshold)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkDexFiles.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkDexFiles.kt
new file mode 100644
index 0000000..6cf8035
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkDexFiles.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+@file:RestrictTo(RestrictTo.Scope.LIBRARY)
+
+package androidx.privacysandbox.sdkruntime.client.loader.storage
+
+import androidx.annotation.RestrictTo
+import java.io.File
+
+/**
+ * Represent SDK Dex files extracted to device storage.
+ */
+internal data class LocalSdkDexFiles(
+    val files: List<File>
+)
+
+/**
+ * Convert [LocalSdkDexFiles] to ClassPath string.
+ */
+internal fun LocalSdkDexFiles.toClassPathString() =
+    files.joinToString(separator = File.pathSeparator, transform = File::getPath)
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkFolderProvider.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkFolderProvider.kt
new file mode 100644
index 0000000..72aaffb
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkFolderProvider.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.storage
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Build.VERSION_CODES.TIRAMISU
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.File
+
+/**
+ * Create folders for Local SDKs in ([Context.getCacheDir] / RuntimeEnabledSdk / <packageName>)
+ *
+ * Store Application update time ([android.content.pm.PackageInfo.lastUpdateTime]) in
+ * ([Context.getCacheDir] / RuntimeEnabledSdk / Folder.version) file.
+ * Remove SDK Folders if Application was updated after folders were created.
+ */
+internal class LocalSdkFolderProvider private constructor(
+    private val sdkRootFolder: File
+) {
+
+    /**
+     * Return folder on storage that should be used for storing SDK DEX files.
+     */
+    fun dexFolderFor(sdkConfig: LocalSdkConfig): File {
+        val sdkDexFolder = File(sdkRootFolder, sdkConfig.packageName)
+        if (!sdkDexFolder.exists()) {
+            sdkDexFolder.mkdirs()
+        }
+        return sdkDexFolder
+    }
+
+    companion object {
+
+        private const val SDK_ROOT_FOLDER = "RuntimeEnabledSdk"
+        private const val VERSION_FILE_NAME = "Folder.version"
+
+        /**
+         * Create LocalSdkFolderProvider.
+         *
+         * Check if current root folder created in same app installation
+         * and remove folder content if not.
+         */
+        fun create(context: Context): LocalSdkFolderProvider {
+            val sdkRootFolder = createSdkRootFolder(context)
+            return LocalSdkFolderProvider(sdkRootFolder)
+        }
+
+        private fun createSdkRootFolder(context: Context): File {
+            val rootFolder = File(context.cacheDir, SDK_ROOT_FOLDER)
+            val versionFile = File(rootFolder, VERSION_FILE_NAME)
+
+            val sdkRootFolderVersion = readVersion(versionFile)
+            val lastUpdateTime = appLastUpdateTime(context)
+
+            if (lastUpdateTime != sdkRootFolderVersion) {
+                if (rootFolder.exists()) {
+                    rootFolder.deleteRecursively()
+                }
+                rootFolder.mkdirs()
+                versionFile.createNewFile()
+
+                versionFile.outputStream().use { outputStream ->
+                    DataOutputStream(outputStream).use { dataStream ->
+                        dataStream.writeLong(lastUpdateTime)
+                    }
+                }
+            }
+
+            return rootFolder
+        }
+
+        private fun appLastUpdateTime(context: Context): Long {
+            if (Build.VERSION.SDK_INT >= TIRAMISU) {
+                return Api33Impl.getLastUpdateTime(context)
+            }
+
+            @Suppress("DEPRECATION")
+            val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
+            return packageInfo.lastUpdateTime
+        }
+
+        private fun readVersion(versionFile: File): Long? {
+            if (!versionFile.exists()) {
+                return null
+            }
+            try {
+                versionFile.inputStream().use { inputStream ->
+                    DataInputStream(inputStream).use { dataStream ->
+                        return dataStream.readLong()
+                    }
+                }
+            } catch (e: Exception) {
+                // Failed to parse or IOException, treat as no version file exists.
+                return null
+            }
+        }
+    }
+
+    @RequiresApi(TIRAMISU)
+    private object Api33Impl {
+        @DoNotInline
+        fun getLastUpdateTime(context: Context): Long =
+            context.packageManager.getPackageInfo(
+                context.packageName,
+                PackageManager.PackageInfoFlags.of(0)
+            ).lastUpdateTime
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkStorage.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkStorage.kt
new file mode 100644
index 0000000..66fdab7
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/main/java/androidx/privacysandbox/sdkruntime/client/loader/storage/LocalSdkStorage.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader.storage
+
+import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
+
+/**
+ * Provides interface for getting SDK related files.
+ */
+internal interface LocalSdkStorage {
+    /**
+     * Get [LocalSdkDexFiles] for bundled SDK.
+     *
+     * @param sdkConfig sdk config
+     * @return [LocalSdkDexFiles] if DEX files available or null if not.
+     */
+    fun dexFilesFor(sdkConfig: LocalSdkConfig): LocalSdkDexFiles?
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/test/java/androidx/privacysandbox/sdkruntime/client/loader/ResourceRemappingTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/test/java/androidx/privacysandbox/sdkruntime/client/loader/ResourceRemappingTest.kt
new file mode 100644
index 0000000..cb17a81
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/test/java/androidx/privacysandbox/sdkruntime/client/loader/ResourceRemappingTest.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.privacysandbox.sdkruntime.client.loader
+
+import androidx.privacysandbox.sdkruntime.client.config.ResourceRemappingConfig
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.compile
+import com.google.common.truth.Truth.assertThat
+import java.net.URLClassLoader
+import kotlin.reflect.KClass
+import org.junit.Assert.assertThrows
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ResourceRemappingTest {
+
+    @field:Rule
+    @JvmField
+    val temporaryFolder = TemporaryFolder()
+
+    @Test
+    fun apply_whenNonNullConfig_updatePackageId() {
+        val classLoader = compileAndLoad(
+            Source.java(
+                "RPackage", """
+                   public class RPackage {
+                        public static int packageId = 0;
+                   }
+                """
+            )
+        )
+
+        ResourceRemapping.apply(
+            classLoader,
+            ResourceRemappingConfig(
+                rPackageClassName = "RPackage",
+                packageId = 42
+            )
+        )
+
+        val rPackageClass = classLoader.loadClass("RPackage")
+        val packageIdField = rPackageClass.getDeclaredField("packageId")
+        val value = packageIdField.getInt(null)
+
+        assertThat(value).isEqualTo(42)
+    }
+
+    @Test
+    fun apply_whenNullConfig_doesntThrow() {
+        val classLoader = compileAndLoad(
+            Source.java(
+                "AnotherClass", """
+                   public class AnotherClass {
+                   }
+                """
+            )
+        )
+
+        ResourceRemapping.apply(
+            classLoader,
+            remappingConfig = null
+        )
+    }
+
+    @Test
+    fun apply_whenNoRPackageClass_throwsClassNotFoundException() {
+        val source = Source.java(
+            "AnotherClass", """
+                public class AnotherClass {
+                }
+                """
+        )
+
+        val config = ResourceRemappingConfig(
+            rPackageClassName = "RPackage",
+            packageId = 42
+        )
+
+        assertThrows(ClassNotFoundException::class, source, config)
+    }
+
+    @Test
+    fun apply_whenNoPackageIdField_throwsNoSuchFieldException() {
+        val source = Source.java(
+            "RPackage", """
+                   public class RPackage {
+                   }
+                """
+        )
+
+        val config = ResourceRemappingConfig(
+            rPackageClassName = "RPackage",
+            packageId = 42
+        )
+
+        assertThrows(NoSuchFieldException::class, source, config)
+    }
+
+    private fun assertThrows(
+        expectedThrowable: KClass<out Exception>,
+        source: Source,
+        config: ResourceRemappingConfig
+    ) {
+        val classLoader = compileAndLoad(source)
+        assertThrows(expectedThrowable.java) {
+            ResourceRemapping.apply(
+                classLoader,
+                config
+            )
+        }
+    }
+
+    private fun compileAndLoad(source: Source): ClassLoader {
+        val compilationResult = compile(
+            temporaryFolder.root,
+            TestCompilationArguments(
+                sources = listOf(source),
+            )
+        )
+
+        assertThat(compilationResult.success).isTrue()
+
+        return URLClassLoader.newInstance(
+            compilationResult.outputClasspath.map {
+                it.toURI().toURL()
+            }.toTypedArray(),
+            /* parent = */ null
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
index e6f50d0..cc05185 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/current.txt
@@ -1 +1,50 @@
 // Signature format: 4.0
+package androidx.privacysandbox.sdkruntime.core {
+
+  public final class LoadSdkCompatException extends java.lang.Exception {
+    ctor public LoadSdkCompatException(Throwable cause, android.os.Bundle extraInfo);
+    method public android.os.Bundle getExtraInformation();
+    method public int getLoadSdkErrorCode();
+    property public final android.os.Bundle extraInformation;
+    property public final int loadSdkErrorCode;
+    field public static final androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion Companion;
+    field public static final int LOAD_SDK_ALREADY_LOADED = 101; // 0x65
+    field public static final int LOAD_SDK_INTERNAL_ERROR = 500; // 0x1f4
+    field public static final int LOAD_SDK_NOT_FOUND = 100; // 0x64
+    field public static final int LOAD_SDK_SDK_DEFINED_ERROR = 102; // 0x66
+    field public static final int LOAD_SDK_SDK_SANDBOX_DISABLED = 103; // 0x67
+    field public static final int SDK_SANDBOX_PROCESS_NOT_AVAILABLE = 503; // 0x1f7
+  }
+
+  public static final class LoadSdkCompatException.Companion {
+  }
+
+  public final class SandboxedSdkCompat {
+    ctor public SandboxedSdkCompat(android.os.IBinder sdkInterface);
+    method public static androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat create(android.os.IBinder binder);
+    method public android.os.IBinder? getInterface();
+    field public static final androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion Companion;
+  }
+
+  public static final class SandboxedSdkCompat.Companion {
+    method public androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat create(android.os.IBinder binder);
+  }
+
+  @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
+    ctor public SandboxedSdkProviderAdapter();
+    method public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
+  }
+
+  public abstract class SandboxedSdkProviderCompat {
+    ctor public SandboxedSdkProviderCompat();
+    method public final void attachContext(android.content.Context context);
+    method public void beforeUnloadSdk();
+    method public final android.content.Context? getContext();
+    method public abstract android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public abstract androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat onLoadSdk(android.os.Bundle params) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
+    property public final android.content.Context? context;
+  }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt
index e6f50d0..cc05185 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/public_plus_experimental_current.txt
@@ -1 +1,50 @@
 // Signature format: 4.0
+package androidx.privacysandbox.sdkruntime.core {
+
+  public final class LoadSdkCompatException extends java.lang.Exception {
+    ctor public LoadSdkCompatException(Throwable cause, android.os.Bundle extraInfo);
+    method public android.os.Bundle getExtraInformation();
+    method public int getLoadSdkErrorCode();
+    property public final android.os.Bundle extraInformation;
+    property public final int loadSdkErrorCode;
+    field public static final androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion Companion;
+    field public static final int LOAD_SDK_ALREADY_LOADED = 101; // 0x65
+    field public static final int LOAD_SDK_INTERNAL_ERROR = 500; // 0x1f4
+    field public static final int LOAD_SDK_NOT_FOUND = 100; // 0x64
+    field public static final int LOAD_SDK_SDK_DEFINED_ERROR = 102; // 0x66
+    field public static final int LOAD_SDK_SDK_SANDBOX_DISABLED = 103; // 0x67
+    field public static final int SDK_SANDBOX_PROCESS_NOT_AVAILABLE = 503; // 0x1f7
+  }
+
+  public static final class LoadSdkCompatException.Companion {
+  }
+
+  public final class SandboxedSdkCompat {
+    ctor public SandboxedSdkCompat(android.os.IBinder sdkInterface);
+    method public static androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat create(android.os.IBinder binder);
+    method public android.os.IBinder? getInterface();
+    field public static final androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion Companion;
+  }
+
+  public static final class SandboxedSdkCompat.Companion {
+    method public androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat create(android.os.IBinder binder);
+  }
+
+  @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
+    ctor public SandboxedSdkProviderAdapter();
+    method public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
+  }
+
+  public abstract class SandboxedSdkProviderCompat {
+    ctor public SandboxedSdkProviderCompat();
+    method public final void attachContext(android.content.Context context);
+    method public void beforeUnloadSdk();
+    method public final android.content.Context? getContext();
+    method public abstract android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public abstract androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat onLoadSdk(android.os.Bundle params) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
+    property public final android.content.Context? context;
+  }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt b/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
index e6f50d0..cc05185 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/restricted_current.txt
@@ -1 +1,50 @@
 // Signature format: 4.0
+package androidx.privacysandbox.sdkruntime.core {
+
+  public final class LoadSdkCompatException extends java.lang.Exception {
+    ctor public LoadSdkCompatException(Throwable cause, android.os.Bundle extraInfo);
+    method public android.os.Bundle getExtraInformation();
+    method public int getLoadSdkErrorCode();
+    property public final android.os.Bundle extraInformation;
+    property public final int loadSdkErrorCode;
+    field public static final androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion Companion;
+    field public static final int LOAD_SDK_ALREADY_LOADED = 101; // 0x65
+    field public static final int LOAD_SDK_INTERNAL_ERROR = 500; // 0x1f4
+    field public static final int LOAD_SDK_NOT_FOUND = 100; // 0x64
+    field public static final int LOAD_SDK_SDK_DEFINED_ERROR = 102; // 0x66
+    field public static final int LOAD_SDK_SDK_SANDBOX_DISABLED = 103; // 0x67
+    field public static final int SDK_SANDBOX_PROCESS_NOT_AVAILABLE = 503; // 0x1f7
+  }
+
+  public static final class LoadSdkCompatException.Companion {
+  }
+
+  public final class SandboxedSdkCompat {
+    ctor public SandboxedSdkCompat(android.os.IBinder sdkInterface);
+    method public static androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat create(android.os.IBinder binder);
+    method public android.os.IBinder? getInterface();
+    field public static final androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion Companion;
+  }
+
+  public static final class SandboxedSdkCompat.Companion {
+    method public androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat create(android.os.IBinder binder);
+  }
+
+  @RequiresExtension(extension=android.os.ext.SdkExtensions.AD_SERVICES, version=4) public final class SandboxedSdkProviderAdapter extends android.app.sdksandbox.SandboxedSdkProvider {
+    ctor public SandboxedSdkProviderAdapter();
+    method public android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkException::class) public android.app.sdksandbox.SandboxedSdk onLoadSdk(android.os.Bundle params) throws android.app.sdksandbox.LoadSdkException;
+  }
+
+  public abstract class SandboxedSdkProviderCompat {
+    ctor public SandboxedSdkProviderCompat();
+    method public final void attachContext(android.content.Context context);
+    method public void beforeUnloadSdk();
+    method public final android.content.Context? getContext();
+    method public abstract android.view.View getView(android.content.Context windowContext, android.os.Bundle params, int width, int height);
+    method @kotlin.jvm.Throws(exceptionClasses=LoadSdkCompatException::class) public abstract androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat onLoadSdk(android.os.Bundle params) throws androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException;
+    property public final android.content.Context? context;
+  }
+
+}
+
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
index bbd8542..f976dc3 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-core/build.gradle
@@ -24,9 +24,30 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    api(projectOrArtifact(":annotation:annotation"))
+
+    implementation("androidx.core:core:1.8.0")
+
+    // TODO(b/249982004): cleanup dependencies
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.junit)
+
+    androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
 }
 
 android {
+    lintOptions {
+        // All components could be loaded from another app via client library
+        disable("BanKeepAnnotation")
+    }
+
+    compileSdk = 33
+    compileSdkExtension = 4
     namespace "androidx.privacysandbox.sdkruntime.core"
 }
 
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/AndroidManifest.xml b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..3f2e804
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/assets/SandboxedSdkProviderCompatClassName.txt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/assets/SandboxedSdkProviderCompatClassName.txt
new file mode 100644
index 0000000..3d062e4
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/assets/SandboxedSdkProviderCompatClassName.txt
@@ -0,0 +1 @@
+androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderAdapterTest$TestOnLoadReturnResultSdkProvider
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/LoadSdkCompatExceptionTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/LoadSdkCompatExceptionTest.kt
new file mode 100644
index 0000000..9235febc
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/LoadSdkCompatExceptionTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.LoadSdkException
+import android.os.Build.VERSION_CODES.TIRAMISU
+import android.os.Bundle
+import android.os.ext.SdkExtensions.AD_SERVICES
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException.Companion.toLoadCompatSdkException
+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.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+@SuppressLint("NewApi")
+// TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+@RequiresExtension(extension = AD_SERVICES, version = 4)
+@SdkSuppress(minSdkVersion = TIRAMISU)
+class LoadSdkCompatExceptionTest {
+
+    @Before
+    fun setUp() {
+        assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
+    }
+
+    @Test
+    fun toLoadSdkException_returnLoadSdkException() {
+        val loadSdkCompatException = LoadSdkCompatException(RuntimeException(), Bundle())
+
+        val loadSdkException = loadSdkCompatException.toLoadSdkException()
+
+        assertThat(loadSdkException.cause)
+            .isSameInstanceAs(loadSdkCompatException.cause)
+        assertThat(loadSdkException.extraInformation)
+            .isSameInstanceAs(loadSdkCompatException.extraInformation)
+        assertThat(loadSdkException.loadSdkErrorCode)
+            .isEqualTo(loadSdkCompatException.loadSdkErrorCode)
+    }
+
+    @Test
+    fun toLoadCompatSdkException_returnLoadCompatSdkException() {
+        val loadSdkException = LoadSdkException(
+            RuntimeException(),
+            Bundle()
+        )
+
+        val loadCompatSdkException = toLoadCompatSdkException(loadSdkException)
+
+        assertThat(loadCompatSdkException.cause)
+            .isSameInstanceAs(loadSdkException.cause)
+        assertThat(loadCompatSdkException.extraInformation)
+            .isSameInstanceAs(loadSdkException.extraInformation)
+        assertThat(loadCompatSdkException.loadSdkErrorCode)
+            .isEqualTo(loadSdkException.loadSdkErrorCode)
+    }
+
+    private fun isSandboxApiAvailable() =
+        AdServicesInfo.version() >= 4
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompatTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompatTest.kt
new file mode 100644
index 0000000..195b68a
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompatTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.SandboxedSdk
+import android.os.Binder
+import android.os.Build.VERSION_CODES.TIRAMISU
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+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.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SandboxedSdkCompatTest {
+
+    @Test
+    fun getInterface_returnsBinderPassedToCreate() {
+        val binder = Binder()
+
+        val sandboxedSdkCompat = SandboxedSdkCompat(binder)
+
+        assertThat(sandboxedSdkCompat.getInterface())
+            .isSameInstanceAs(binder)
+    }
+
+    @Test
+    // TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+    @SuppressLint("NewApi")
+    // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    @SdkSuppress(minSdkVersion = TIRAMISU)
+    fun toSandboxedSdk_whenCreatedFromBinder_returnsSandboxedSdkWithSameBinder() {
+        assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
+
+        val binder = Binder()
+
+        val toSandboxedSdkResult = SandboxedSdkCompat(binder).toSandboxedSdk()
+
+        assertThat(toSandboxedSdkResult.getInterface())
+            .isSameInstanceAs(binder)
+    }
+
+    @Test
+    // TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+    @SuppressLint("NewApi")
+    // TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    @SdkSuppress(minSdkVersion = TIRAMISU)
+    fun toSandboxedSdk_whenCreatedFromSandboxedSdk_returnsSameSandboxedSdk() {
+        assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
+
+        val binder = Binder()
+        val sandboxedSdk = SandboxedSdk(binder)
+
+        val toSandboxedSdkResult = SandboxedSdkCompat(sandboxedSdk).toSandboxedSdk()
+
+        assertThat(toSandboxedSdkResult)
+            .isSameInstanceAs(sandboxedSdk)
+    }
+
+    private fun isSandboxApiAvailable() =
+        AdServicesInfo.version() >= 4
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapterTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapterTest.kt
new file mode 100644
index 0000000..db9b56b
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapterTest.kt
@@ -0,0 +1,263 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.LoadSdkException
+import android.content.Context
+import android.os.Binder
+import android.os.Build.VERSION_CODES.TIRAMISU
+import android.os.Bundle
+import android.os.ext.SdkExtensions.AD_SERVICES
+import android.view.View
+import androidx.annotation.RequiresExtension
+import androidx.test.core.app.ApplicationProvider
+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 kotlin.reflect.KClass
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+// TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+@SuppressLint("NewApi")
+// TODO(b/262577044) Remove RequiresExtension after extensions support in @SdkSuppress
+@RequiresExtension(extension = AD_SERVICES, version = 4)
+@SdkSuppress(minSdkVersion = TIRAMISU)
+class SandboxedSdkProviderAdapterTest {
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        assumeTrue("Requires Sandbox API available", isSandboxApiAvailable())
+        context = ApplicationProvider.getApplicationContext()
+    }
+
+    @Test
+    fun testAdapterGetCompatClassNameFromAsset() {
+        val expectedClassName = context.assets
+            .open("SandboxedSdkProviderCompatClassName.txt")
+            .use { inputStream ->
+                inputStream.bufferedReader().readLine()
+            }
+
+        val adapter = SandboxedSdkProviderAdapter()
+        adapter.attachContext(context)
+
+        adapter.onLoadSdk(Bundle())
+
+        val delegate = adapter.extractDelegate<SandboxedSdkProviderCompat>()
+        assertThat(delegate.javaClass.name)
+            .isEqualTo(expectedClassName)
+    }
+
+    @Test
+    fun onLoadSdk_shouldInstantiateDelegateAndAttachContext() {
+        val adapter = createAdapterFor(TestOnLoadReturnResultSdkProvider::class)
+
+        adapter.onLoadSdk(Bundle())
+
+        val delegate = adapter.extractDelegate<TestOnLoadReturnResultSdkProvider>()
+        assertThat(delegate.context)
+            .isSameInstanceAs(context)
+    }
+
+    @Test
+    fun onLoadSdk_shouldDelegateToCompatClassAndReturnResult() {
+        val adapter = createAdapterFor(TestOnLoadReturnResultSdkProvider::class)
+        val params = Bundle()
+
+        val result = adapter.onLoadSdk(params)
+
+        val delegate = adapter.extractDelegate<TestOnLoadReturnResultSdkProvider>()
+        assertThat(delegate.mLastOnLoadSdkBundle)
+            .isSameInstanceAs(params)
+        assertThat(result.getInterface())
+            .isEqualTo(delegate.mResult.getInterface())
+    }
+
+    @Test
+    fun loadSdk_shouldRethrowExceptionFromCompatClass() {
+        val adapter = createAdapterFor(TestOnLoadThrowSdkProvider::class)
+
+        val ex = assertThrows(LoadSdkException::class.java) {
+            adapter.onLoadSdk(Bundle())
+        }
+
+        val delegate = adapter.extractDelegate<TestOnLoadThrowSdkProvider>()
+        assertThat(ex.cause)
+            .isSameInstanceAs(delegate.mError.cause)
+        assertThat(ex.extraInformation)
+            .isSameInstanceAs(delegate.mError.extraInformation)
+    }
+
+    @Test
+    fun loadSdk_shouldThrowIfCompatClassNotExists() {
+        val adapter = createAdapterFor("NOTEXISTS")
+
+        assertThrows(ClassNotFoundException::class.java) {
+            adapter.onLoadSdk(Bundle())
+        }
+    }
+
+    @Test
+    fun beforeUnloadSdk_shouldDelegateToCompatProvider() {
+        val adapter = createAdapterFor(TestOnBeforeUnloadDelegateSdkProvider::class)
+
+        adapter.beforeUnloadSdk()
+
+        val delegate = adapter.extractDelegate<TestOnBeforeUnloadDelegateSdkProvider>()
+        assertThat(delegate.mBeforeUnloadSdkCalled)
+            .isTrue()
+    }
+
+    @Test
+    fun getView_shouldDelegateToCompatProviderAndReturnResult() {
+        val adapter = createAdapterFor(TestGetViewSdkProvider::class)
+        val windowContext = mock(Context::class.java)
+        val params = Bundle()
+        val width = 1
+        val height = 2
+
+        val result = adapter.getView(windowContext, params, width, height)
+
+        val delegate = adapter.extractDelegate<TestGetViewSdkProvider>()
+        assertThat(result)
+            .isSameInstanceAs(delegate.mView)
+        assertThat(delegate.mLastWindowContext)
+            .isSameInstanceAs(windowContext)
+        assertThat(delegate.mLastParams)
+            .isSameInstanceAs(params)
+        assertThat(delegate.mLastWidth)
+            .isSameInstanceAs(width)
+        assertThat(delegate.mLastHeigh)
+            .isSameInstanceAs(height)
+    }
+
+    private fun createAdapterFor(
+        clazz: KClass<out SandboxedSdkProviderCompat>
+    ): SandboxedSdkProviderAdapter = createAdapterFor(clazz.java.name)
+
+    private fun createAdapterFor(delegateClassName: String): SandboxedSdkProviderAdapter {
+        val adapter = SandboxedSdkProviderAdapter(
+            object : SandboxedSdkProviderAdapter.CompatClassNameProvider {
+                override fun getCompatProviderClassName(context: Context): String {
+                    return delegateClassName
+                }
+            })
+        adapter.attachContext(context)
+        return adapter
+    }
+
+    private inline fun <reified T : SandboxedSdkProviderCompat>
+        SandboxedSdkProviderAdapter.extractDelegate(): T = delegate as T
+
+    class TestOnLoadReturnResultSdkProvider : SandboxedSdkProviderCompat() {
+        var mResult = SandboxedSdkCompat(Binder())
+        var mLastOnLoadSdkBundle: Bundle? = null
+
+        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+            mLastOnLoadSdkBundle = params
+            return mResult
+        }
+
+        override fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+        ): View {
+            throw RuntimeException("Not implemented")
+        }
+    }
+
+    class TestOnLoadThrowSdkProvider : SandboxedSdkProviderCompat() {
+        var mError = LoadSdkCompatException(RuntimeException(), Bundle())
+
+        @Throws(LoadSdkCompatException::class)
+        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+            throw mError
+        }
+
+        override fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+        ): View {
+            throw RuntimeException("Stub!")
+        }
+    }
+
+    class TestOnBeforeUnloadDelegateSdkProvider : SandboxedSdkProviderCompat() {
+        var mBeforeUnloadSdkCalled = false
+
+        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+            throw RuntimeException("Not implemented")
+        }
+
+        override fun beforeUnloadSdk() {
+            mBeforeUnloadSdkCalled = true
+        }
+
+        override fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+        ): View {
+            throw RuntimeException("Not implemented")
+        }
+    }
+
+    class TestGetViewSdkProvider : SandboxedSdkProviderCompat() {
+        val mView: View = mock(View::class.java)
+
+        var mLastWindowContext: Context? = null
+        var mLastParams: Bundle? = null
+        var mLastWidth = 0
+        var mLastHeigh = 0
+
+        override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
+            throw RuntimeException("Not implemented")
+        }
+
+        override fun getView(
+            windowContext: Context,
+            params: Bundle,
+            width: Int,
+            height: Int
+        ): View {
+            mLastWindowContext = windowContext
+            mLastParams = params
+            mLastWidth = width
+            mLastHeigh = height
+
+            return mView
+        }
+    }
+
+    private fun isSandboxApiAvailable() =
+        AdServicesInfo.version() >= 4
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/AdServicesInfo.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/AdServicesInfo.kt
new file mode 100644
index 0000000..c43ea33
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/AdServicesInfo.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.os.Build
+import android.os.ext.SdkExtensions
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+
+/**
+ * Temporary replacement for BuildCompat.AD_SERVICES_EXTENSION_INT.
+ * TODO(b/249981547) Replace with AD_SERVICES_EXTENSION_INT after new core library release
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object AdServicesInfo {
+
+    fun version(): Int {
+        return if (Build.VERSION.SDK_INT >= 30) {
+            Extensions30Impl.getAdServicesVersion()
+        } else {
+            0
+        }
+    }
+
+    @RequiresApi(30)
+    private object Extensions30Impl {
+        @DoNotInline
+        fun getAdServicesVersion() =
+            SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/LoadSdkCompatException.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/LoadSdkCompatException.kt
new file mode 100644
index 0000000..b1851c4
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/LoadSdkCompatException.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.LoadSdkException
+import android.os.Bundle
+import android.os.ext.SdkExtensions.AD_SERVICES
+import androidx.annotation.DoNotInline
+import androidx.annotation.IntDef
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+
+/**
+ * Compat alternative for [LoadSdkException].
+ * Thrown from [SandboxedSdkProviderCompat.onLoadSdk].
+ *
+ * @see [LoadSdkException]
+ */
+class LoadSdkCompatException : Exception {
+
+    /**
+     * Result code this exception was constructed with.
+     *
+     * @see [LoadSdkException.getLoadSdkErrorCode]
+     */
+    @field:LoadSdkErrorCode
+    @get:LoadSdkErrorCode
+    val loadSdkErrorCode: Int
+
+    /**
+     * Extra error information this exception was constructed with.
+     *
+     * @see [LoadSdkException.getExtraInformation]
+     */
+    val extraInformation: Bundle
+
+    /**
+     * Initializes a LoadSdkCompatException with a result code, a message, a cause and extra
+     * information.
+     *
+     * @param loadSdkErrorCode The result code.
+     * @param message The detailed message.
+     * @param cause The cause of the exception. A null value is permitted, and indicates that the
+     *  cause is nonexistent or unknown.
+     * @param extraInformation Extra error information. This is empty if there is no such information.
+     * @suppress
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    @JvmOverloads
+    constructor(
+        @LoadSdkErrorCode loadSdkErrorCode: Int,
+        message: String?,
+        cause: Throwable?,
+        extraInformation: Bundle = Bundle()
+    ) : super(message, cause) {
+        this.loadSdkErrorCode = loadSdkErrorCode
+        this.extraInformation = extraInformation
+    }
+
+    /**
+     * Initializes a LoadSdkCompatException with a result code and a message
+     *
+     * @param loadSdkErrorCode The result code.
+     * @param message The detailed message.
+     * @suppress
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    constructor(
+        @LoadSdkErrorCode loadSdkErrorCode: Int,
+        message: String?
+    ) : this(loadSdkErrorCode, message, cause = null)
+
+    /**
+     * Initializes a LoadSdkCompatException with a Throwable and a Bundle.
+     *
+     * @param cause The cause of the exception.
+     * @param extraInfo Extra error information. This is empty if there is no such information.
+     */
+    constructor(
+        cause: Throwable,
+        extraInfo: Bundle
+    ) : this(LOAD_SDK_SDK_DEFINED_ERROR, "", cause, extraInfo)
+
+    /** @suppress */
+    @IntDef(
+        SDK_SANDBOX_PROCESS_NOT_AVAILABLE,
+        LOAD_SDK_NOT_FOUND,
+        LOAD_SDK_ALREADY_LOADED,
+        LOAD_SDK_SDK_DEFINED_ERROR,
+        LOAD_SDK_SDK_SANDBOX_DISABLED,
+        LOAD_SDK_INTERNAL_ERROR,
+    )
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class LoadSdkErrorCode
+
+    /**
+     *  Create platform [LoadSdkException] from compat exception.
+     *
+     *  @return Platform exception.
+     */
+    @RequiresExtension(extension = AD_SERVICES, version = 4)
+    internal fun toLoadSdkException(): LoadSdkException {
+        return ApiAdServicesV4Impl.toLoadSdkException(this)
+    }
+
+    // TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = AD_SERVICES, version = 4)
+    private object ApiAdServicesV4Impl {
+
+        @DoNotInline
+        fun toLoadSdkException(ex: LoadSdkCompatException): LoadSdkException {
+            return LoadSdkException(
+                ex.cause!!,
+                ex.extraInformation
+            )
+        }
+
+        @DoNotInline
+        fun toLoadCompatSdkException(ex: LoadSdkException): LoadSdkCompatException {
+            return LoadSdkCompatException(
+                toLoadSdkErrorCodeCompat(ex.loadSdkErrorCode),
+                ex.message,
+                ex.cause,
+                ex.extraInformation
+            )
+        }
+
+        @LoadSdkErrorCode
+        private fun toLoadSdkErrorCodeCompat(
+            value: Int
+        ): Int {
+            return value // TODO(b/249982002): Validate and convert
+        }
+    }
+
+    companion object {
+
+        /**
+         * Sdk sandbox process is not available.
+         *
+         * This indicates that the sdk sandbox process is not available, either because it has died,
+         * disconnected or was not created in the first place.
+         *
+         * @see [android.app.sdksandbox.SdkSandboxManager.SDK_SANDBOX_PROCESS_NOT_AVAILABLE]
+         */
+        const val SDK_SANDBOX_PROCESS_NOT_AVAILABLE = 503
+
+        /**
+         * SDK not found.
+         *
+         * This indicates that client application tried to load a non-existing SDK.
+         *
+         * @see [android.app.sdksandbox.SdkSandboxManager.LOAD_SDK_NOT_FOUND]
+         */
+        const val LOAD_SDK_NOT_FOUND = 100
+
+        /**
+         * SDK is already loaded.
+         *
+         * This indicates that client application tried to reload the same SDK after being
+         * successfully loaded.
+         *
+         * @see [android.app.sdksandbox.SdkSandboxManager.LOAD_SDK_ALREADY_LOADED]
+         */
+        const val LOAD_SDK_ALREADY_LOADED = 101
+
+        /**
+         * SDK error after being loaded.
+         *
+         * This indicates that the SDK encountered an error during post-load initialization. The
+         * details of this can be obtained from the Bundle returned in [LoadSdkCompatException].
+         *
+         * @see [android.app.sdksandbox.SdkSandboxManager.LOAD_SDK_SDK_DEFINED_ERROR]
+         */
+        const val LOAD_SDK_SDK_DEFINED_ERROR = 102
+
+        /**
+         * SDK sandbox is disabled.
+         *
+         * This indicates that the SDK sandbox is disabled. Any subsequent attempts to load SDKs in
+         * this boot will also fail.
+         *
+         * @see [android.app.sdksandbox.SdkSandboxManager.LOAD_SDK_SDK_SANDBOX_DISABLED]
+         */
+        const val LOAD_SDK_SDK_SANDBOX_DISABLED = 103
+
+        /**
+         * Internal error while loading SDK.
+         *
+         * This indicates a generic internal error happened while applying the call from
+         * client application.
+         *
+         * @see [android.app.sdksandbox.SdkSandboxManager.LOAD_SDK_INTERNAL_ERROR]
+         */
+        const val LOAD_SDK_INTERNAL_ERROR = 500
+
+        /**
+         *  Create compat exception from platform [LoadSdkException].
+         *
+         *  @param ex Platform exception
+         *  @return Compat exception.
+         *  @suppress
+         */
+        @RequiresExtension(extension = AD_SERVICES, version = 4)
+        @RestrictTo(LIBRARY_GROUP)
+        fun toLoadCompatSdkException(ex: LoadSdkException): LoadSdkCompatException {
+            return ApiAdServicesV4Impl.toLoadCompatSdkException(ex)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt
new file mode 100644
index 0000000..54bce19
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.SandboxedSdk
+import android.os.IBinder
+import android.os.ext.SdkExtensions.AD_SERVICES
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresExtension
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+
+/**
+ * Compat wrapper for [SandboxedSdk].
+ * Represents an SDK loaded in the sandbox process or locally.
+ * An application should use this object to obtain an interface to the SDK through [getInterface].
+ *
+ * The SDK should create it when [SandboxedSdkProviderCompat.onLoadSdk] is called, and drop all
+ * references to it when [SandboxedSdkProviderCompat.beforeUnloadSdk] is called. Additionally, the
+ * SDK should fail calls made to the [IBinder] returned from [getInterface] after
+ * [SandboxedSdkProviderCompat.beforeUnloadSdk] has been called.
+ *
+ * @see [SandboxedSdk]
+ *
+ */
+class SandboxedSdkCompat private constructor(
+    private val sdkImpl: SandboxedSdkImpl
+) {
+
+    /**
+     * Creates SandboxedSdkCompat from SDK Binder object.
+     *
+     * @param sdkInterface The SDK's interface. This will be the entrypoint into the sandboxed SDK
+     * for the application. The SDK should keep this valid until it's loaded in the sandbox, and
+     * start failing calls to this interface once it has been unloaded
+     *
+     * This interface can later be retrieved using [getInterface].
+     *
+     * @see [SandboxedSdk]
+     */
+    constructor(sdkInterface: IBinder) : this(CompatImpl(sdkInterface))
+
+    /**
+     * Creates SandboxedSdkCompat wrapper around existing [SandboxedSdk] object.
+     *
+     * @param sandboxedSdk SandboxedSdk object. All calls will be delegated to that object.
+     * @suppress
+     */
+    @RequiresExtension(extension = AD_SERVICES, version = 4)
+    @RestrictTo(LIBRARY_GROUP)
+    constructor(sandboxedSdk: SandboxedSdk) : this(ApiAdServicesV4Impl(sandboxedSdk))
+
+    /**
+     * Returns the interface to the loaded SDK.
+     * A null interface is returned if the Binder has since
+     * become unavailable, in response to the SDK being unloaded.
+     *
+     * @return [IBinder] object for loaded SDK.
+     *
+     * @see [SandboxedSdk.getInterface]
+     */
+    fun getInterface() = sdkImpl.getInterface()
+
+    /**
+     * Create [SandboxedSdk] from compat object.
+     *
+     * @return Platform SandboxedSdk
+     */
+    @RequiresExtension(extension = AD_SERVICES, version = 4)
+    internal fun toSandboxedSdk() = sdkImpl.toSandboxedSdk()
+
+    internal interface SandboxedSdkImpl {
+        fun getInterface(): IBinder?
+
+        @RequiresExtension(extension = AD_SERVICES, version = 4)
+        @DoNotInline
+        fun toSandboxedSdk(): SandboxedSdk
+    }
+
+    // TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+    @SuppressLint("NewApi", "ClassVerificationFailure")
+    @RequiresExtension(extension = AD_SERVICES, version = 4)
+    private class ApiAdServicesV4Impl(private val mSandboxedSdk: SandboxedSdk) : SandboxedSdkImpl {
+
+        @DoNotInline
+        override fun getInterface(): IBinder? {
+            return mSandboxedSdk.getInterface()
+        }
+
+        @DoNotInline
+        override fun toSandboxedSdk(): SandboxedSdk {
+            return mSandboxedSdk
+        }
+
+        companion object {
+            @DoNotInline
+            fun createSandboxedSdk(sdkInterface: IBinder): SandboxedSdk {
+                return SandboxedSdk(sdkInterface)
+            }
+        }
+    }
+
+    private class CompatImpl(private val sdkInterface: IBinder) : SandboxedSdkImpl {
+
+        override fun getInterface(): IBinder {
+            // This will be null if the SDK has been unloaded and the IBinder originally provided
+            // is now a dead object.
+            return sdkInterface
+        }
+
+        @RequiresExtension(extension = AD_SERVICES, version = 4)
+        override fun toSandboxedSdk(): SandboxedSdk {
+            // avoid class verifications errors
+            return ApiAdServicesV4Impl.createSandboxedSdk(sdkInterface)
+        }
+    }
+
+    companion object {
+        /**
+         *  Deprecated and will be removed in next release.
+         *  Use [SandboxedSdkCompat] constructor instead.
+         *  TODO(b/261013990) Remove method after Shim generator migration and release
+         */
+        @JvmStatic
+        fun create(binder: IBinder): SandboxedSdkCompat = SandboxedSdkCompat(binder)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt
new file mode 100644
index 0000000..d0af98f
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderAdapter.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.annotation.SuppressLint
+import android.app.sdksandbox.LoadSdkException
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SandboxedSdkProvider
+import android.content.Context
+import android.os.Bundle
+import android.os.ext.SdkExtensions.AD_SERVICES
+import android.view.View
+import androidx.annotation.RequiresExtension
+
+/**
+ * Implementation of platform [SandboxedSdkProvider] that delegate to [SandboxedSdkProviderCompat]
+ * Gets compat class name from asset "SandboxedSdkProviderCompatClassName.txt"
+ *
+ */
+// TODO(b/249981547) Remove suppress after updating to new lint version (b/262251309)
+@SuppressLint("NewApi", "ClassVerificationFailure", "Override")
+@RequiresExtension(extension = AD_SERVICES, version = 4)
+class SandboxedSdkProviderAdapter internal constructor(
+    private val classNameProvider: CompatClassNameProvider
+) : SandboxedSdkProvider() {
+
+    /**
+     * Provides classname of [SandboxedSdkProviderCompat] implementation.
+     */
+    internal interface CompatClassNameProvider {
+        fun getCompatProviderClassName(context: Context): String
+    }
+
+    constructor () : this(DefaultClassNameProvider())
+
+    internal val delegate: SandboxedSdkProviderCompat by lazy {
+        val currentContext = context!!
+        val compatSdkProviderClassName =
+            classNameProvider.getCompatProviderClassName(currentContext)
+        val clz = Class.forName(compatSdkProviderClassName)
+        val newDelegate = clz.getConstructor().newInstance() as SandboxedSdkProviderCompat
+        newDelegate.attachContext(currentContext)
+        newDelegate
+    }
+
+    @Throws(LoadSdkException::class)
+    override fun onLoadSdk(params: Bundle): SandboxedSdk {
+        return try {
+            delegate.onLoadSdk(params).toSandboxedSdk()
+        } catch (e: LoadSdkCompatException) {
+            throw e.toLoadSdkException()
+        }
+    }
+
+    override fun beforeUnloadSdk() {
+        delegate.beforeUnloadSdk()
+    }
+
+    override fun getView(
+        windowContext: Context,
+        params: Bundle,
+        width: Int,
+        height: Int
+    ): View {
+        return delegate.getView(windowContext, params, width, height)
+    }
+
+    private class DefaultClassNameProvider : CompatClassNameProvider {
+        override fun getCompatProviderClassName(context: Context): String {
+            // TODO(b/257966930) Read classname from SDK manifest property
+            return context.assets.open(COMPAT_SDK_PROVIDER_CLASS_ASSET_NAME)
+                .use { inputStream ->
+                    inputStream.bufferedReader().readLine()
+                }
+        }
+    }
+
+    private companion object {
+        private const val COMPAT_SDK_PROVIDER_CLASS_ASSET_NAME =
+            "SandboxedSdkProviderCompatClassName.txt"
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt
new file mode 100644
index 0000000..8882d6f
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+
+/**
+ * Compat version of [android.app.sdksandbox.SandboxedSdkProvider].
+ *
+ * Encapsulates API which SDK sandbox can use to interact with SDKs loaded into it.
+ *
+ * SDK has to implement this abstract class to generate an entry point for SDK sandbox to be able
+ *  to call it through.
+ *
+ * @see [android.app.sdksandbox.SandboxedSdkProvider]
+ */
+abstract class SandboxedSdkProviderCompat {
+    /**
+     * Context previously set through [SandboxedSdkProviderCompat.attachContext].
+     * This will return null if no context has been previously set.
+     */
+    var context: Context? = null
+        private set
+
+    /**
+     * Sets the SDK [Context] which can then be received using [SandboxedSdkProviderCompat.context]
+     *
+     * This is called before [SandboxedSdkProviderCompat.onLoadSdk] is invoked.
+     * No operations requiring a [Context] should be performed before then, as
+     * [SandboxedSdkProviderCompat.context] will return null until this method has been called.
+     *
+     * @throws IllegalStateException if a base context has already been set.
+     *
+     * @param context The new base context.
+     *
+     * @see [android.app.sdksandbox.SandboxedSdkProvider.attachContext]
+     */
+    fun attachContext(context: Context) {
+        check(this.context == null) { "Context already set" }
+        this.context = context
+    }
+
+    /**
+     * Does the work needed for the SDK to start handling requests.
+     *
+     * This function is called by the SDK sandbox after it loads the SDK.
+     *
+     * SDK should do any work to be ready to handle upcoming requests. It should not do any
+     * long-running tasks here, like I/O and network calls. Doing so can prevent the SDK from
+     * receiving requests from the client. Additionally, it should not do initialization that
+     * depends on other SDKs being loaded into the SDK sandbox.
+     *
+     * The SDK should not do any operations requiring a [Context] object before this method
+     * has been called.
+     *
+     * @param params list of params passed from the client when it loads the SDK. This can be empty.
+     * @return Returns a [SandboxedSdkCompat], passed back to the client. The IBinder used to create
+     * the [SandboxedSdkCompat] object will be used by the client to call into the SDK.
+     *
+     * @throws LoadSdkCompatException if initialization failed.
+     *
+     * @see [android.app.sdksandbox.SandboxedSdkProvider.onLoadSdk]
+     */
+    @Throws(LoadSdkCompatException::class)
+    abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+
+    /**
+     * Does the work needed for the SDK to free its resources before being unloaded.
+     *
+     * This function is called by the SDK sandbox manager before it unloads the SDK. The SDK
+     * should fail any invocations on the Binder previously returned to the client through
+     * [SandboxedSdkCompat.getInterface]
+     *
+     * The SDK should not do any long-running tasks here, like I/O and network calls.
+     *
+     * @see [android.app.sdksandbox.SandboxedSdkProvider.beforeUnloadSdk]
+     */
+    open fun beforeUnloadSdk() {}
+
+    /**
+     * Requests a view to be remotely rendered to the client app process.
+     *
+     * @see [android.app.sdksandbox.SandboxedSdkProvider.getView]
+     */
+    abstract fun getView(
+        windowContext: Context,
+        params: Bundle,
+        width: Int,
+        height: Int
+    ): View
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
new file mode 100644
index 0000000..d683bdf
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/Versions.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.privacysandbox.sdkruntime.core
+
+import androidx.annotation.Keep
+import androidx.annotation.RestrictTo
+
+/**
+ * Store internal API version (for Client-Core communication).
+ * Methods invoked via reflection.
+ *
+ * @suppress
+ */
+@Suppress("unused")
+@Keep
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object Versions {
+
+    const val API_VERSION = 1
+
+    @JvmField
+    var CLIENT_VERSION: Int? = null
+
+    @JvmStatic
+    fun handShake(clientVersion: Int): Int {
+        CLIENT_VERSION = clientVersion
+        return API_VERSION
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/androidx/privacysandbox/sdkruntime/androidx-privacysandbox-sdkruntime-sdkruntime-core-documentation.md b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/androidx-privacysandbox-sdkruntime-sdkruntime-core-documentation.md
similarity index 100%
rename from privacysandbox/sdkruntime/sdkruntime-core/src/main/androidx/privacysandbox/sdkruntime/androidx-privacysandbox-sdkruntime-sdkruntime-core-documentation.md
rename to privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/androidx-privacysandbox-sdkruntime-sdkruntime-core-documentation.md
diff --git a/recyclerview/recyclerview/api/api_lint.ignore b/recyclerview/recyclerview/api/api_lint.ignore
index ee7fa16..463599f 100644
--- a/recyclerview/recyclerview/api/api_lint.ignore
+++ b/recyclerview/recyclerview/api/api_lint.ignore
@@ -161,12 +161,6 @@
     Internal field mLayoutManager must not be exposed
 
 
-InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerView#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `c` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerView#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
-    Invalid nullability on parameter `canvas` in method `drawChild`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerView#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `c` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate.ItemDelegate#dispatchPopulateAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent) parameter #0:
     Invalid nullability on parameter `host` in method `dispatchPopulateAccessibilityEvent`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate.ItemDelegate#dispatchPopulateAccessibilityEvent(android.view.View, android.view.accessibility.AccessibilityEvent) parameter #1:
@@ -551,6 +545,10 @@
     Missing nullability on parameter `container` in method `dispatchRestoreInstanceState`
 MissingNullability: androidx.recyclerview.widget.RecyclerView#dispatchSaveInstanceState(android.util.SparseArray<android.os.Parcelable>) parameter #0:
     Missing nullability on parameter `container` in method `dispatchSaveInstanceState`
+MissingNullability: androidx.recyclerview.widget.RecyclerView#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `c` in method `draw`
+MissingNullability: androidx.recyclerview.widget.RecyclerView#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
+    Missing nullability on parameter `canvas` in method `drawChild`
 MissingNullability: androidx.recyclerview.widget.RecyclerView#drawChild(android.graphics.Canvas, android.view.View, long) parameter #1:
     Missing nullability on parameter `child` in method `drawChild`
 MissingNullability: androidx.recyclerview.widget.RecyclerView#findViewHolderForItemId(long):
@@ -573,6 +571,8 @@
     Missing nullability on method `getAccessibilityClassName` return
 MissingNullability: androidx.recyclerview.widget.RecyclerView#getChildViewHolder(android.view.View):
     Missing nullability on method `getChildViewHolder` return
+MissingNullability: androidx.recyclerview.widget.RecyclerView#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `c` in method `onDraw`
 MissingNullability: androidx.recyclerview.widget.RecyclerView#onGenericMotionEvent(android.view.MotionEvent) parameter #0:
     Missing nullability on parameter `event` in method `onGenericMotionEvent`
 MissingNullability: androidx.recyclerview.widget.RecyclerView#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
diff --git a/settings.gradle b/settings.gradle
index 6b0a412..e8e5ddc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -92,9 +92,9 @@
         value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
         value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
 
-        // Publish scan for androidx-main
-        publishAlways()
-        publishIfAuthenticated()
+        // Do not publish scan for androidx-platform-dev
+        // publishAlways()
+        // publishIfAuthenticated()
     }
 }
 
@@ -794,6 +794,8 @@
 includeProject(":preference:preference", [BuildType.MAIN])
 includeProject(":preference:preference-ktx", [BuildType.MAIN])
 includeProject(":print:print", [BuildType.MAIN])
+includeProject(":privacysandbox:ads:ads-adservices", [BuildType.MAIN])
+includeProject(":privacysandbox:ads:ads-adservices-java", [BuildType.MAIN])
 includeProject(":privacysandbox:sdkruntime:sdkruntime-client", [BuildType.MAIN])
 includeProject(":privacysandbox:sdkruntime:sdkruntime-core", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools", [BuildType.MAIN])
@@ -962,6 +964,7 @@
 includeProject(":webkit:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":webkit:webkit", [BuildType.MAIN])
 includeProject(":window:window", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window-samples", "window/window/samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":window:extensions:extensions", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":window:extensions:core:core", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":window:integration-tests:configuration-change-tests", [BuildType.MAIN])
@@ -970,7 +973,9 @@
 includeProject(":window:window-core", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":window:window-rxjava2", [BuildType.MAIN])
 includeProject(":window:window-rxjava3", [BuildType.MAIN])
-includeProject(":window:window-samples", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window-demos:demo", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window-demos:demo-common", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
+includeProject(":window:window-demos:demo-second-app", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":window:window-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN, BuildType.CAMERA])
 includeProject(":work:integration-tests:testapp", [BuildType.MAIN])
 includeProject(":work:work-benchmark", [BuildType.MAIN])
diff --git a/slidingpanelayout/slidingpanelayout/api/api_lint.ignore b/slidingpanelayout/slidingpanelayout/api/api_lint.ignore
index e495753..a288bd0 100644
--- a/slidingpanelayout/slidingpanelayout/api/api_lint.ignore
+++ b/slidingpanelayout/slidingpanelayout/api/api_lint.ignore
@@ -1,10 +1,6 @@
 // Baseline format: 1.0
 InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#addView(android.view.View, int, android.view.ViewGroup.LayoutParams) parameter #0:
     Invalid nullability on parameter `child` in method `addView`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `c` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
-    Invalid nullability on parameter `canvas` in method `drawChild`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 InvalidNullabilityOverride: androidx.slidingpanelayout.widget.SlidingPaneLayout#removeView(android.view.View) parameter #0:
     Invalid nullability on parameter `view` in method `removeView`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 
@@ -15,6 +11,10 @@
 
 MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#checkLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
     Missing nullability on parameter `p` in method `checkLayoutParams`
+MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `c` in method `draw`
+MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #0:
+    Missing nullability on parameter `canvas` in method `drawChild`
 MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#drawChild(android.graphics.Canvas, android.view.View, long) parameter #1:
     Missing nullability on parameter `child` in method `drawChild`
 MissingNullability: androidx.slidingpanelayout.widget.SlidingPaneLayout#generateDefaultLayoutParams():
diff --git a/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore b/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore
index 6038f08..25a9da5 100644
--- a/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore
+++ b/swiperefreshlayout/swiperefreshlayout/api/api_lint.ignore
@@ -13,6 +13,8 @@
     Internal field mOriginalOffsetTop must not be exposed
 
 
+MissingNullability: androidx.swiperefreshlayout.widget.CircularProgressDrawable#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.swiperefreshlayout.widget.CircularProgressDrawable#setColorFilter(android.graphics.ColorFilter) parameter #0:
     Missing nullability on parameter `colorFilter` in method `setColorFilter`
 MissingNullability: androidx.swiperefreshlayout.widget.SwipeRefreshLayout#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index efa3607..b4e08aec 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -27,10 +27,10 @@
     object Metadata {
         const val version = "1.0.0-alpha09"
         val checksums = mapOf(
-            "arm64-v8a" to "130cea2d90e941acb9d2087c0bebe8229b461175454df20763bfe901c0d73155",
-            "armeabi-v7a" to "cb0e60c5d8726d274bbc678cfa3a8724e66d2bfa6874c06c5f4347e545c0439d",
-            "x86" to "c6b19ce773d17c74aa74aa813637bc1d2bd61f5bcc280e6a5c030a36948dd6dd",
-            "x86_64" to "af62d109588db30c8ce11f2027227664520372f8c77259d4155dd92f7cc73a6e",
+            "arm64-v8a" to "49f7d5d8b9c960a6e2d0de99675746a24bbd0aa28de0370691b5ebe5a8c99a10",
+            "armeabi-v7a" to "5410fdae5c774d5938b7ebcf6b127e89ec4729a09d26efe448a86ae000353aa4",
+            "x86" to "38c40e69f274ba32beefabec853888486a533fb8301e3f832648e0e16d73121b",
+            "x86_64" to "19cb5840489d287ad290bb62e1a5e2bbe6a885f31ea3cee3cf2984ccd39ceaa7",
         )
     }
 
diff --git a/viewpager/viewpager/api/api_lint.ignore b/viewpager/viewpager/api/api_lint.ignore
index 1c07b48..908ecb2 100644
--- a/viewpager/viewpager/api/api_lint.ignore
+++ b/viewpager/viewpager/api/api_lint.ignore
@@ -9,18 +9,12 @@
     Symmetric method for `setDrawFullUnderline` must be named `isDrawFullUnderline`; was `getDrawFullUnderline`
 
 
-InvalidNullabilityOverride: androidx.viewpager.widget.PagerTabStrip#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.viewpager.widget.ViewPager#draw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `draw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.viewpager.widget.ViewPager#onDraw(android.graphics.Canvas) parameter #0:
-    Invalid nullability on parameter `canvas` in method `onDraw`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-
-
 ListenerInterface: androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener:
     Listeners should be an interface, or otherwise renamed Callback: SimpleOnPageChangeListener
 
 
+MissingNullability: androidx.viewpager.widget.PagerTabStrip#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.viewpager.widget.PagerTabStrip#onTouchEvent(android.view.MotionEvent) parameter #0:
     Missing nullability on parameter `ev` in method `onTouchEvent`
 MissingNullability: androidx.viewpager.widget.PagerTabStrip#setBackgroundDrawable(android.graphics.drawable.Drawable) parameter #0:
@@ -39,6 +33,8 @@
     Missing nullability on parameter `event` in method `dispatchKeyEvent`
 MissingNullability: androidx.viewpager.widget.ViewPager#dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent) parameter #0:
     Missing nullability on parameter `event` in method `dispatchPopulateAccessibilityEvent`
+MissingNullability: androidx.viewpager.widget.ViewPager#draw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `draw`
 MissingNullability: androidx.viewpager.widget.ViewPager#generateDefaultLayoutParams():
     Missing nullability on method `generateDefaultLayoutParams` return
 MissingNullability: androidx.viewpager.widget.ViewPager#generateLayoutParams(android.util.AttributeSet):
@@ -49,6 +45,8 @@
     Missing nullability on method `generateLayoutParams` return
 MissingNullability: androidx.viewpager.widget.ViewPager#generateLayoutParams(android.view.ViewGroup.LayoutParams) parameter #0:
     Missing nullability on parameter `p` in method `generateLayoutParams`
+MissingNullability: androidx.viewpager.widget.ViewPager#onDraw(android.graphics.Canvas) parameter #0:
+    Missing nullability on parameter `canvas` in method `onDraw`
 MissingNullability: androidx.viewpager.widget.ViewPager#onInterceptTouchEvent(android.view.MotionEvent) parameter #0:
     Missing nullability on parameter `ev` in method `onInterceptTouchEvent`
 MissingNullability: androidx.viewpager.widget.ViewPager#onRequestFocusInDescendants(int, android.graphics.Rect) parameter #1:
diff --git a/webkit/webkit/api/1.6.0-beta02.txt b/webkit/webkit/api/1.6.0-beta02.txt
deleted file mode 100644
index faf13cb..0000000
--- a/webkit/webkit/api/1.6.0-beta02.txt
+++ /dev/null
@@ -1,300 +0,0 @@
-// Signature format: 4.0
-package androidx.webkit {
-
-  public class CookieManagerCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
-  }
-
-  public abstract class JavaScriptReplyProxy {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
-  }
-
-  public class ProcessGlobalConfig {
-    ctor public ProcessGlobalConfig();
-    method public static void apply(androidx.webkit.ProcessGlobalConfig);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
-  }
-
-  public final class ProxyConfig {
-    method public java.util.List<java.lang.String!> getBypassRules();
-    method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
-    method public boolean isReverseBypassEnabled();
-    field public static final String MATCH_ALL_SCHEMES = "*";
-    field public static final String MATCH_HTTP = "http";
-    field public static final String MATCH_HTTPS = "https";
-  }
-
-  public static final class ProxyConfig.Builder {
-    ctor public ProxyConfig.Builder();
-    ctor public ProxyConfig.Builder(androidx.webkit.ProxyConfig);
-    method public androidx.webkit.ProxyConfig.Builder addBypassRule(String);
-    method public androidx.webkit.ProxyConfig.Builder addDirect(String);
-    method public androidx.webkit.ProxyConfig.Builder addDirect();
-    method public androidx.webkit.ProxyConfig.Builder addProxyRule(String);
-    method public androidx.webkit.ProxyConfig.Builder addProxyRule(String, String);
-    method public androidx.webkit.ProxyConfig build();
-    method public androidx.webkit.ProxyConfig.Builder bypassSimpleHostnames();
-    method public androidx.webkit.ProxyConfig.Builder removeImplicitRules();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public androidx.webkit.ProxyConfig.Builder setReverseBypassEnabled(boolean);
-  }
-
-  public static final class ProxyConfig.ProxyRule {
-    method public String getSchemeFilter();
-    method public String getUrl();
-  }
-
-  public abstract class ProxyController {
-    method public abstract void clearProxyOverride(java.util.concurrent.Executor, Runnable);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ProxyController getInstance();
-    method public abstract void setProxyOverride(androidx.webkit.ProxyConfig, java.util.concurrent.Executor, Runnable);
-  }
-
-  public abstract class SafeBrowsingResponseCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void backToSafety(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void proceed(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
-  }
-
-  public abstract class ServiceWorkerClientCompat {
-    ctor public ServiceWorkerClientCompat();
-    method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
-  }
-
-  public abstract class ServiceWorkerControllerCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
-    method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
-    method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat?);
-  }
-
-  public abstract class ServiceWorkerWebSettingsCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowContentAccess();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
-  }
-
-  public class TracingConfig {
-    method public java.util.List<java.lang.String!> getCustomIncludedCategories();
-    method public int getPredefinedCategories();
-    method public int getTracingMode();
-    field public static final int CATEGORIES_ALL = 1; // 0x1
-    field public static final int CATEGORIES_ANDROID_WEBVIEW = 2; // 0x2
-    field public static final int CATEGORIES_FRAME_VIEWER = 64; // 0x40
-    field public static final int CATEGORIES_INPUT_LATENCY = 8; // 0x8
-    field public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 32; // 0x20
-    field public static final int CATEGORIES_NONE = 0; // 0x0
-    field public static final int CATEGORIES_RENDERING = 16; // 0x10
-    field public static final int CATEGORIES_WEB_DEVELOPER = 4; // 0x4
-    field public static final int RECORD_CONTINUOUSLY = 1; // 0x1
-    field public static final int RECORD_UNTIL_FULL = 0; // 0x0
-  }
-
-  public static class TracingConfig.Builder {
-    ctor public TracingConfig.Builder();
-    method public androidx.webkit.TracingConfig.Builder addCategories(int...);
-    method public androidx.webkit.TracingConfig.Builder addCategories(java.lang.String!...);
-    method public androidx.webkit.TracingConfig.Builder addCategories(java.util.Collection<java.lang.String!>);
-    method public androidx.webkit.TracingConfig build();
-    method public androidx.webkit.TracingConfig.Builder setTracingMode(int);
-  }
-
-  public abstract class TracingController {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.TracingController getInstance();
-    method public abstract boolean isTracing();
-    method public abstract void start(androidx.webkit.TracingConfig);
-    method public abstract boolean stop(java.io.OutputStream?, java.util.concurrent.Executor);
-  }
-
-  public class WebMessageCompat {
-    ctor public WebMessageCompat(String?);
-    ctor public WebMessageCompat(String?, androidx.webkit.WebMessagePortCompat![]?);
-    method public String? getData();
-    method public androidx.webkit.WebMessagePortCompat![]? getPorts();
-  }
-
-  public abstract class WebMessagePortCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_CLOSE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void close();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(androidx.webkit.WebMessageCompat);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(android.os.Handler?, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
-  }
-
-  public abstract static class WebMessagePortCompat.WebMessageCallbackCompat {
-    ctor public WebMessagePortCompat.WebMessageCallbackCompat();
-    method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat?);
-  }
-
-  public abstract class WebResourceErrorCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract CharSequence getDescription();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getErrorCode();
-  }
-
-  public class WebResourceRequestCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isRedirect(android.webkit.WebResourceRequest);
-  }
-
-  public class WebSettingsCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings, boolean);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
-    field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
-    field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
-    field @Deprecated public static final int DARK_STRATEGY_WEB_THEME_DARKENING_ONLY = 1; // 0x1
-    field @Deprecated public static final int FORCE_DARK_AUTO = 1; // 0x1
-    field @Deprecated public static final int FORCE_DARK_OFF = 0; // 0x0
-    field @Deprecated public static final int FORCE_DARK_ON = 2; // 0x2
-  }
-
-  public final class WebViewAssetLoader {
-    method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
-    field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
-  }
-
-  public static final class WebViewAssetLoader.AssetsPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.AssetsPathHandler(android.content.Context);
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public static final class WebViewAssetLoader.Builder {
-    ctor public WebViewAssetLoader.Builder();
-    method public androidx.webkit.WebViewAssetLoader.Builder addPathHandler(String, androidx.webkit.WebViewAssetLoader.PathHandler);
-    method public androidx.webkit.WebViewAssetLoader build();
-    method public androidx.webkit.WebViewAssetLoader.Builder setDomain(String);
-    method public androidx.webkit.WebViewAssetLoader.Builder setHttpAllowed(boolean);
-  }
-
-  public static final class WebViewAssetLoader.InternalStoragePathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.InternalStoragePathHandler(android.content.Context, java.io.File);
-    method @WorkerThread public android.webkit.WebResourceResponse handle(String);
-  }
-
-  public static interface WebViewAssetLoader.PathHandler {
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public static final class WebViewAssetLoader.ResourcesPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.ResourcesPathHandler(android.content.Context);
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public class WebViewClientCompat extends android.webkit.WebViewClient {
-    ctor public WebViewClientCompat();
-    method @RequiresApi(23) public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
-    method @RequiresApi(21) @UiThread public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
-    method @RequiresApi(27) public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
-    method @UiThread public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
-  }
-
-  public class WebViewCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
-    method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_VARIATIONS_HEADER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static String getVariationsHeader();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_CHROME_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebChromeClient? getWebChromeClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebViewClient getWebViewClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_RENDERER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcess? getWebViewRenderProcess(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcessClient? getWebViewRenderProcessClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void removeWebMessageListener(android.webkit.WebView, String);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ALLOWLIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingAllowlist(java.util.Set<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_WHITELIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, java.util.concurrent.Executor, androidx.webkit.WebViewRenderProcessClient);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, androidx.webkit.WebViewRenderProcessClient?);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.START_SAFE_BROWSING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean!>?);
-  }
-
-  public static interface WebViewCompat.VisualStateCallback {
-    method @UiThread public void onComplete(long);
-  }
-
-  public static interface WebViewCompat.WebMessageListener {
-    method @UiThread public void onPostMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri, boolean, androidx.webkit.JavaScriptReplyProxy);
-  }
-
-  public class WebViewFeature {
-    method public static boolean isFeatureSupported(String);
-    method public static boolean isStartupFeatureSupported(android.content.Context, String);
-    field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
-    field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
-    field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
-    field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
-    field public static final String FORCE_DARK = "FORCE_DARK";
-    field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
-    field public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
-    field public static final String GET_VARIATIONS_HEADER = "GET_VARIATIONS_HEADER";
-    field public static final String GET_WEB_CHROME_CLIENT = "GET_WEB_CHROME_CLIENT";
-    field public static final String GET_WEB_VIEW_CLIENT = "GET_WEB_VIEW_CLIENT";
-    field public static final String GET_WEB_VIEW_RENDERER = "GET_WEB_VIEW_RENDERER";
-    field public static final String MULTI_PROCESS = "MULTI_PROCESS";
-    field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
-    field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
-    field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
-    field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
-    field public static final String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
-    field public static final String SAFE_BROWSING_ALLOWLIST = "SAFE_BROWSING_ALLOWLIST";
-    field public static final String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
-    field public static final String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
-    field public static final String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
-    field public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
-    field public static final String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
-    field public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
-    field @Deprecated public static final String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
-    field public static final String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
-    field public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
-    field public static final String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
-    field public static final String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
-    field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
-    field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
-    field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
-    field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
-    field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
-    field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
-    field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
-    field public static final String WEB_MESSAGE_CALLBACK_ON_MESSAGE = "WEB_MESSAGE_CALLBACK_ON_MESSAGE";
-    field public static final String WEB_MESSAGE_LISTENER = "WEB_MESSAGE_LISTENER";
-    field public static final String WEB_MESSAGE_PORT_CLOSE = "WEB_MESSAGE_PORT_CLOSE";
-    field public static final String WEB_MESSAGE_PORT_POST_MESSAGE = "WEB_MESSAGE_PORT_POST_MESSAGE";
-    field public static final String WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK = "WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK";
-    field public static final String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
-    field public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
-    field public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
-    field public static final String WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE = "WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE";
-    field public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
-  }
-
-  public abstract class WebViewRenderProcess {
-    ctor public WebViewRenderProcess();
-    method public abstract boolean terminate();
-  }
-
-  public abstract class WebViewRenderProcessClient {
-    ctor public WebViewRenderProcessClient();
-    method public abstract void onRenderProcessResponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
-    method public abstract void onRenderProcessUnresponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
-  }
-
-}
-
diff --git a/webkit/webkit/api/public_plus_experimental_1.6.0-beta02.txt b/webkit/webkit/api/public_plus_experimental_1.6.0-beta02.txt
deleted file mode 100644
index faf13cb..0000000
--- a/webkit/webkit/api/public_plus_experimental_1.6.0-beta02.txt
+++ /dev/null
@@ -1,300 +0,0 @@
-// Signature format: 4.0
-package androidx.webkit {
-
-  public class CookieManagerCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
-  }
-
-  public abstract class JavaScriptReplyProxy {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
-  }
-
-  public class ProcessGlobalConfig {
-    ctor public ProcessGlobalConfig();
-    method public static void apply(androidx.webkit.ProcessGlobalConfig);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
-  }
-
-  public final class ProxyConfig {
-    method public java.util.List<java.lang.String!> getBypassRules();
-    method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
-    method public boolean isReverseBypassEnabled();
-    field public static final String MATCH_ALL_SCHEMES = "*";
-    field public static final String MATCH_HTTP = "http";
-    field public static final String MATCH_HTTPS = "https";
-  }
-
-  public static final class ProxyConfig.Builder {
-    ctor public ProxyConfig.Builder();
-    ctor public ProxyConfig.Builder(androidx.webkit.ProxyConfig);
-    method public androidx.webkit.ProxyConfig.Builder addBypassRule(String);
-    method public androidx.webkit.ProxyConfig.Builder addDirect(String);
-    method public androidx.webkit.ProxyConfig.Builder addDirect();
-    method public androidx.webkit.ProxyConfig.Builder addProxyRule(String);
-    method public androidx.webkit.ProxyConfig.Builder addProxyRule(String, String);
-    method public androidx.webkit.ProxyConfig build();
-    method public androidx.webkit.ProxyConfig.Builder bypassSimpleHostnames();
-    method public androidx.webkit.ProxyConfig.Builder removeImplicitRules();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public androidx.webkit.ProxyConfig.Builder setReverseBypassEnabled(boolean);
-  }
-
-  public static final class ProxyConfig.ProxyRule {
-    method public String getSchemeFilter();
-    method public String getUrl();
-  }
-
-  public abstract class ProxyController {
-    method public abstract void clearProxyOverride(java.util.concurrent.Executor, Runnable);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ProxyController getInstance();
-    method public abstract void setProxyOverride(androidx.webkit.ProxyConfig, java.util.concurrent.Executor, Runnable);
-  }
-
-  public abstract class SafeBrowsingResponseCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void backToSafety(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void proceed(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
-  }
-
-  public abstract class ServiceWorkerClientCompat {
-    ctor public ServiceWorkerClientCompat();
-    method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
-  }
-
-  public abstract class ServiceWorkerControllerCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
-    method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
-    method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat?);
-  }
-
-  public abstract class ServiceWorkerWebSettingsCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowContentAccess();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
-  }
-
-  public class TracingConfig {
-    method public java.util.List<java.lang.String!> getCustomIncludedCategories();
-    method public int getPredefinedCategories();
-    method public int getTracingMode();
-    field public static final int CATEGORIES_ALL = 1; // 0x1
-    field public static final int CATEGORIES_ANDROID_WEBVIEW = 2; // 0x2
-    field public static final int CATEGORIES_FRAME_VIEWER = 64; // 0x40
-    field public static final int CATEGORIES_INPUT_LATENCY = 8; // 0x8
-    field public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 32; // 0x20
-    field public static final int CATEGORIES_NONE = 0; // 0x0
-    field public static final int CATEGORIES_RENDERING = 16; // 0x10
-    field public static final int CATEGORIES_WEB_DEVELOPER = 4; // 0x4
-    field public static final int RECORD_CONTINUOUSLY = 1; // 0x1
-    field public static final int RECORD_UNTIL_FULL = 0; // 0x0
-  }
-
-  public static class TracingConfig.Builder {
-    ctor public TracingConfig.Builder();
-    method public androidx.webkit.TracingConfig.Builder addCategories(int...);
-    method public androidx.webkit.TracingConfig.Builder addCategories(java.lang.String!...);
-    method public androidx.webkit.TracingConfig.Builder addCategories(java.util.Collection<java.lang.String!>);
-    method public androidx.webkit.TracingConfig build();
-    method public androidx.webkit.TracingConfig.Builder setTracingMode(int);
-  }
-
-  public abstract class TracingController {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.TracingController getInstance();
-    method public abstract boolean isTracing();
-    method public abstract void start(androidx.webkit.TracingConfig);
-    method public abstract boolean stop(java.io.OutputStream?, java.util.concurrent.Executor);
-  }
-
-  public class WebMessageCompat {
-    ctor public WebMessageCompat(String?);
-    ctor public WebMessageCompat(String?, androidx.webkit.WebMessagePortCompat![]?);
-    method public String? getData();
-    method public androidx.webkit.WebMessagePortCompat![]? getPorts();
-  }
-
-  public abstract class WebMessagePortCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_CLOSE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void close();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(androidx.webkit.WebMessageCompat);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(android.os.Handler?, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
-  }
-
-  public abstract static class WebMessagePortCompat.WebMessageCallbackCompat {
-    ctor public WebMessagePortCompat.WebMessageCallbackCompat();
-    method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat?);
-  }
-
-  public abstract class WebResourceErrorCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract CharSequence getDescription();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getErrorCode();
-  }
-
-  public class WebResourceRequestCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isRedirect(android.webkit.WebResourceRequest);
-  }
-
-  public class WebSettingsCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings, boolean);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
-    field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
-    field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
-    field @Deprecated public static final int DARK_STRATEGY_WEB_THEME_DARKENING_ONLY = 1; // 0x1
-    field @Deprecated public static final int FORCE_DARK_AUTO = 1; // 0x1
-    field @Deprecated public static final int FORCE_DARK_OFF = 0; // 0x0
-    field @Deprecated public static final int FORCE_DARK_ON = 2; // 0x2
-  }
-
-  public final class WebViewAssetLoader {
-    method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
-    field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
-  }
-
-  public static final class WebViewAssetLoader.AssetsPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.AssetsPathHandler(android.content.Context);
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public static final class WebViewAssetLoader.Builder {
-    ctor public WebViewAssetLoader.Builder();
-    method public androidx.webkit.WebViewAssetLoader.Builder addPathHandler(String, androidx.webkit.WebViewAssetLoader.PathHandler);
-    method public androidx.webkit.WebViewAssetLoader build();
-    method public androidx.webkit.WebViewAssetLoader.Builder setDomain(String);
-    method public androidx.webkit.WebViewAssetLoader.Builder setHttpAllowed(boolean);
-  }
-
-  public static final class WebViewAssetLoader.InternalStoragePathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.InternalStoragePathHandler(android.content.Context, java.io.File);
-    method @WorkerThread public android.webkit.WebResourceResponse handle(String);
-  }
-
-  public static interface WebViewAssetLoader.PathHandler {
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public static final class WebViewAssetLoader.ResourcesPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.ResourcesPathHandler(android.content.Context);
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public class WebViewClientCompat extends android.webkit.WebViewClient {
-    ctor public WebViewClientCompat();
-    method @RequiresApi(23) public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
-    method @RequiresApi(21) @UiThread public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
-    method @RequiresApi(27) public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
-    method @UiThread public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
-  }
-
-  public class WebViewCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
-    method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_VARIATIONS_HEADER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static String getVariationsHeader();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_CHROME_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebChromeClient? getWebChromeClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebViewClient getWebViewClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_RENDERER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcess? getWebViewRenderProcess(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcessClient? getWebViewRenderProcessClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void removeWebMessageListener(android.webkit.WebView, String);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ALLOWLIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingAllowlist(java.util.Set<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_WHITELIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, java.util.concurrent.Executor, androidx.webkit.WebViewRenderProcessClient);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, androidx.webkit.WebViewRenderProcessClient?);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.START_SAFE_BROWSING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean!>?);
-  }
-
-  public static interface WebViewCompat.VisualStateCallback {
-    method @UiThread public void onComplete(long);
-  }
-
-  public static interface WebViewCompat.WebMessageListener {
-    method @UiThread public void onPostMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri, boolean, androidx.webkit.JavaScriptReplyProxy);
-  }
-
-  public class WebViewFeature {
-    method public static boolean isFeatureSupported(String);
-    method public static boolean isStartupFeatureSupported(android.content.Context, String);
-    field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
-    field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
-    field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
-    field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
-    field public static final String FORCE_DARK = "FORCE_DARK";
-    field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
-    field public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
-    field public static final String GET_VARIATIONS_HEADER = "GET_VARIATIONS_HEADER";
-    field public static final String GET_WEB_CHROME_CLIENT = "GET_WEB_CHROME_CLIENT";
-    field public static final String GET_WEB_VIEW_CLIENT = "GET_WEB_VIEW_CLIENT";
-    field public static final String GET_WEB_VIEW_RENDERER = "GET_WEB_VIEW_RENDERER";
-    field public static final String MULTI_PROCESS = "MULTI_PROCESS";
-    field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
-    field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
-    field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
-    field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
-    field public static final String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
-    field public static final String SAFE_BROWSING_ALLOWLIST = "SAFE_BROWSING_ALLOWLIST";
-    field public static final String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
-    field public static final String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
-    field public static final String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
-    field public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
-    field public static final String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
-    field public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
-    field @Deprecated public static final String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
-    field public static final String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
-    field public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
-    field public static final String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
-    field public static final String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
-    field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
-    field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
-    field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
-    field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
-    field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
-    field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
-    field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
-    field public static final String WEB_MESSAGE_CALLBACK_ON_MESSAGE = "WEB_MESSAGE_CALLBACK_ON_MESSAGE";
-    field public static final String WEB_MESSAGE_LISTENER = "WEB_MESSAGE_LISTENER";
-    field public static final String WEB_MESSAGE_PORT_CLOSE = "WEB_MESSAGE_PORT_CLOSE";
-    field public static final String WEB_MESSAGE_PORT_POST_MESSAGE = "WEB_MESSAGE_PORT_POST_MESSAGE";
-    field public static final String WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK = "WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK";
-    field public static final String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
-    field public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
-    field public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
-    field public static final String WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE = "WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE";
-    field public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
-  }
-
-  public abstract class WebViewRenderProcess {
-    ctor public WebViewRenderProcess();
-    method public abstract boolean terminate();
-  }
-
-  public abstract class WebViewRenderProcessClient {
-    ctor public WebViewRenderProcessClient();
-    method public abstract void onRenderProcessResponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
-    method public abstract void onRenderProcessUnresponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
-  }
-
-}
-
diff --git a/webkit/webkit/api/restricted_1.6.0-beta02.txt b/webkit/webkit/api/restricted_1.6.0-beta02.txt
deleted file mode 100644
index faf13cb..0000000
--- a/webkit/webkit/api/restricted_1.6.0-beta02.txt
+++ /dev/null
@@ -1,300 +0,0 @@
-// Signature format: 4.0
-package androidx.webkit {
-
-  public class CookieManagerCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_COOKIE_INFO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.List<java.lang.String!> getCookieInfo(android.webkit.CookieManager, String);
-  }
-
-  public abstract class JavaScriptReplyProxy {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
-  }
-
-  public class ProcessGlobalConfig {
-    ctor public ProcessGlobalConfig();
-    method public static void apply(androidx.webkit.ProcessGlobalConfig);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
-  }
-
-  public final class ProxyConfig {
-    method public java.util.List<java.lang.String!> getBypassRules();
-    method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
-    method public boolean isReverseBypassEnabled();
-    field public static final String MATCH_ALL_SCHEMES = "*";
-    field public static final String MATCH_HTTP = "http";
-    field public static final String MATCH_HTTPS = "https";
-  }
-
-  public static final class ProxyConfig.Builder {
-    ctor public ProxyConfig.Builder();
-    ctor public ProxyConfig.Builder(androidx.webkit.ProxyConfig);
-    method public androidx.webkit.ProxyConfig.Builder addBypassRule(String);
-    method public androidx.webkit.ProxyConfig.Builder addDirect(String);
-    method public androidx.webkit.ProxyConfig.Builder addDirect();
-    method public androidx.webkit.ProxyConfig.Builder addProxyRule(String);
-    method public androidx.webkit.ProxyConfig.Builder addProxyRule(String, String);
-    method public androidx.webkit.ProxyConfig build();
-    method public androidx.webkit.ProxyConfig.Builder bypassSimpleHostnames();
-    method public androidx.webkit.ProxyConfig.Builder removeImplicitRules();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public androidx.webkit.ProxyConfig.Builder setReverseBypassEnabled(boolean);
-  }
-
-  public static final class ProxyConfig.ProxyRule {
-    method public String getSchemeFilter();
-    method public String getUrl();
-  }
-
-  public abstract class ProxyController {
-    method public abstract void clearProxyOverride(java.util.concurrent.Executor, Runnable);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.PROXY_OVERRIDE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ProxyController getInstance();
-    method public abstract void setProxyOverride(androidx.webkit.ProxyConfig, java.util.concurrent.Executor, Runnable);
-  }
-
-  public abstract class SafeBrowsingResponseCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void backToSafety(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void proceed(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void showInterstitial(boolean);
-  }
-
-  public abstract class ServiceWorkerClientCompat {
-    ctor public ServiceWorkerClientCompat();
-    method @WorkerThread public abstract android.webkit.WebResourceResponse? shouldInterceptRequest(android.webkit.WebResourceRequest);
-  }
-
-  public abstract class ServiceWorkerControllerCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
-    method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
-    method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat?);
-  }
-
-  public abstract class ServiceWorkerWebSettingsCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowContentAccess();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
-  }
-
-  public class TracingConfig {
-    method public java.util.List<java.lang.String!> getCustomIncludedCategories();
-    method public int getPredefinedCategories();
-    method public int getTracingMode();
-    field public static final int CATEGORIES_ALL = 1; // 0x1
-    field public static final int CATEGORIES_ANDROID_WEBVIEW = 2; // 0x2
-    field public static final int CATEGORIES_FRAME_VIEWER = 64; // 0x40
-    field public static final int CATEGORIES_INPUT_LATENCY = 8; // 0x8
-    field public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 32; // 0x20
-    field public static final int CATEGORIES_NONE = 0; // 0x0
-    field public static final int CATEGORIES_RENDERING = 16; // 0x10
-    field public static final int CATEGORIES_WEB_DEVELOPER = 4; // 0x4
-    field public static final int RECORD_CONTINUOUSLY = 1; // 0x1
-    field public static final int RECORD_UNTIL_FULL = 0; // 0x0
-  }
-
-  public static class TracingConfig.Builder {
-    ctor public TracingConfig.Builder();
-    method public androidx.webkit.TracingConfig.Builder addCategories(int...);
-    method public androidx.webkit.TracingConfig.Builder addCategories(java.lang.String!...);
-    method public androidx.webkit.TracingConfig.Builder addCategories(java.util.Collection<java.lang.String!>);
-    method public androidx.webkit.TracingConfig build();
-    method public androidx.webkit.TracingConfig.Builder setTracingMode(int);
-  }
-
-  public abstract class TracingController {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.TracingController getInstance();
-    method public abstract boolean isTracing();
-    method public abstract void start(androidx.webkit.TracingConfig);
-    method public abstract boolean stop(java.io.OutputStream?, java.util.concurrent.Executor);
-  }
-
-  public class WebMessageCompat {
-    ctor public WebMessageCompat(String?);
-    ctor public WebMessageCompat(String?, androidx.webkit.WebMessagePortCompat![]?);
-    method public String? getData();
-    method public androidx.webkit.WebMessagePortCompat![]? getPorts();
-  }
-
-  public abstract class WebMessagePortCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_CLOSE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void close();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(androidx.webkit.WebMessageCompat);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setWebMessageCallback(android.os.Handler?, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
-  }
-
-  public abstract static class WebMessagePortCompat.WebMessageCallbackCompat {
-    ctor public WebMessagePortCompat.WebMessageCallbackCompat();
-    method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat?);
-  }
-
-  public abstract class WebResourceErrorCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract CharSequence getDescription();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getErrorCode();
-  }
-
-  public class WebResourceRequestCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isRedirect(android.webkit.WebResourceRequest);
-  }
-
-  public class WebSettingsCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setEnterpriseAuthenticationAppLinkPolicyEnabled(android.webkit.WebSettings, boolean);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
-    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
-    field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
-    field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
-    field @Deprecated public static final int DARK_STRATEGY_WEB_THEME_DARKENING_ONLY = 1; // 0x1
-    field @Deprecated public static final int FORCE_DARK_AUTO = 1; // 0x1
-    field @Deprecated public static final int FORCE_DARK_OFF = 0; // 0x0
-    field @Deprecated public static final int FORCE_DARK_ON = 2; // 0x2
-  }
-
-  public final class WebViewAssetLoader {
-    method @WorkerThread public android.webkit.WebResourceResponse? shouldInterceptRequest(android.net.Uri);
-    field public static final String DEFAULT_DOMAIN = "appassets.androidplatform.net";
-  }
-
-  public static final class WebViewAssetLoader.AssetsPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.AssetsPathHandler(android.content.Context);
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public static final class WebViewAssetLoader.Builder {
-    ctor public WebViewAssetLoader.Builder();
-    method public androidx.webkit.WebViewAssetLoader.Builder addPathHandler(String, androidx.webkit.WebViewAssetLoader.PathHandler);
-    method public androidx.webkit.WebViewAssetLoader build();
-    method public androidx.webkit.WebViewAssetLoader.Builder setDomain(String);
-    method public androidx.webkit.WebViewAssetLoader.Builder setHttpAllowed(boolean);
-  }
-
-  public static final class WebViewAssetLoader.InternalStoragePathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.InternalStoragePathHandler(android.content.Context, java.io.File);
-    method @WorkerThread public android.webkit.WebResourceResponse handle(String);
-  }
-
-  public static interface WebViewAssetLoader.PathHandler {
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public static final class WebViewAssetLoader.ResourcesPathHandler implements androidx.webkit.WebViewAssetLoader.PathHandler {
-    ctor public WebViewAssetLoader.ResourcesPathHandler(android.content.Context);
-    method @WorkerThread public android.webkit.WebResourceResponse? handle(String);
-  }
-
-  public class WebViewClientCompat extends android.webkit.WebViewClient {
-    ctor public WebViewClientCompat();
-    method @RequiresApi(23) public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
-    method @RequiresApi(21) @UiThread public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
-    method @RequiresApi(27) public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
-    method @UiThread public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
-  }
-
-  public class WebViewCompat {
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void addWebMessageListener(android.webkit.WebView, String, java.util.Set<java.lang.String!>, androidx.webkit.WebViewCompat.WebMessageListener);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebMessagePortCompat![] createWebMessageChannel(android.webkit.WebView);
-    method public static android.content.pm.PackageInfo? getCurrentWebViewPackage(android.content.Context);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_VARIATIONS_HEADER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static String getVariationsHeader();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_CHROME_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebChromeClient? getWebChromeClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_CLIENT, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static android.webkit.WebViewClient getWebViewClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.GET_WEB_VIEW_RENDERER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcess? getWebViewRenderProcess(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static androidx.webkit.WebViewRenderProcessClient? getWebViewRenderProcessClient(android.webkit.WebView);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void removeWebMessageListener(android.webkit.WebView, String);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ALLOWLIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingAllowlist(java.util.Set<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
-    method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_WHITELIST, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String!>, android.webkit.ValueCallback<java.lang.Boolean!>?);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, java.util.concurrent.Executor, androidx.webkit.WebViewRenderProcessClient);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setWebViewRenderProcessClient(android.webkit.WebView, androidx.webkit.WebViewRenderProcessClient?);
-    method @RequiresFeature(name=androidx.webkit.WebViewFeature.START_SAFE_BROWSING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean!>?);
-  }
-
-  public static interface WebViewCompat.VisualStateCallback {
-    method @UiThread public void onComplete(long);
-  }
-
-  public static interface WebViewCompat.WebMessageListener {
-    method @UiThread public void onPostMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri, boolean, androidx.webkit.JavaScriptReplyProxy);
-  }
-
-  public class WebViewFeature {
-    method public static boolean isFeatureSupported(String);
-    method public static boolean isStartupFeatureSupported(android.content.Context, String);
-    field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
-    field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
-    field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
-    field public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
-    field public static final String FORCE_DARK = "FORCE_DARK";
-    field public static final String FORCE_DARK_STRATEGY = "FORCE_DARK_STRATEGY";
-    field public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
-    field public static final String GET_VARIATIONS_HEADER = "GET_VARIATIONS_HEADER";
-    field public static final String GET_WEB_CHROME_CLIENT = "GET_WEB_CHROME_CLIENT";
-    field public static final String GET_WEB_VIEW_CLIENT = "GET_WEB_VIEW_CLIENT";
-    field public static final String GET_WEB_VIEW_RENDERER = "GET_WEB_VIEW_RENDERER";
-    field public static final String MULTI_PROCESS = "MULTI_PROCESS";
-    field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
-    field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
-    field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
-    field public static final String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
-    field public static final String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
-    field public static final String SAFE_BROWSING_ALLOWLIST = "SAFE_BROWSING_ALLOWLIST";
-    field public static final String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
-    field public static final String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
-    field public static final String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
-    field public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
-    field public static final String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
-    field public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
-    field @Deprecated public static final String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
-    field public static final String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
-    field public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
-    field public static final String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
-    field public static final String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
-    field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
-    field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
-    field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
-    field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
-    field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
-    field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
-    field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
-    field public static final String WEB_MESSAGE_CALLBACK_ON_MESSAGE = "WEB_MESSAGE_CALLBACK_ON_MESSAGE";
-    field public static final String WEB_MESSAGE_LISTENER = "WEB_MESSAGE_LISTENER";
-    field public static final String WEB_MESSAGE_PORT_CLOSE = "WEB_MESSAGE_PORT_CLOSE";
-    field public static final String WEB_MESSAGE_PORT_POST_MESSAGE = "WEB_MESSAGE_PORT_POST_MESSAGE";
-    field public static final String WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK = "WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK";
-    field public static final String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
-    field public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
-    field public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
-    field public static final String WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE = "WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE";
-    field public static final String WEB_VIEW_RENDERER_TERMINATE = "WEB_VIEW_RENDERER_TERMINATE";
-  }
-
-  public abstract class WebViewRenderProcess {
-    ctor public WebViewRenderProcess();
-    method public abstract boolean terminate();
-  }
-
-  public abstract class WebViewRenderProcessClient {
-    ctor public WebViewRenderProcessClient();
-    method public abstract void onRenderProcessResponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
-    method public abstract void onRenderProcessUnresponsive(android.webkit.WebView, androidx.webkit.WebViewRenderProcess?);
-  }
-
-}
-
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index 8867f5f6c..449d9aa 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -4,6 +4,7 @@
   public interface WindowExtensions {
     method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
+    method public default androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
 
@@ -13,12 +14,30 @@
 
 }
 
+package androidx.window.extensions.area {
+
+  public interface WindowAreaComponent {
+    method public void addRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void endRearDisplaySession();
+    method public void removeRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void startRearDisplaySession(android.app.Activity, java.util.function.Consumer<java.lang.Integer!>);
+    field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+    field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+    field public static final int STATUS_AVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+  }
+
+}
+
 package androidx.window.extensions.embedding {
 
   public interface ActivityEmbeddingComponent {
+    method public void clearSplitAttributesCalculator();
     method public void clearSplitInfoCallback();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+    method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
     method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
 
@@ -32,6 +51,7 @@
     ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
     method public androidx.window.extensions.embedding.ActivityRule build();
     method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+    method public androidx.window.extensions.embedding.ActivityRule.Builder setTag(String);
   }
 
   public class ActivityStack {
@@ -41,13 +61,68 @@
   }
 
   public abstract class EmbeddingRule {
+    method public String? getTag();
+  }
+
+  public class SplitAttributes {
+    method @ColorInt public int getAnimationBackgroundColor();
+    method public int getLayoutDirection();
+    method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
+  }
+
+  public static final class SplitAttributes.Builder {
+    ctor public SplitAttributes.Builder();
+    method public androidx.window.extensions.embedding.SplitAttributes build();
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
+    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);
+  }
+
+  public static final class SplitAttributes.LayoutDirection {
+    field public static final int BOTTOM_TO_TOP = 5; // 0x5
+    field public static final int LEFT_TO_RIGHT = 0; // 0x0
+    field public static final int LOCALE = 3; // 0x3
+    field public static final int RIGHT_TO_LEFT = 1; // 0x1
+    field public static final int TOP_TO_BOTTOM = 4; // 0x4
+  }
+
+  public static class SplitAttributes.SplitType {
+  }
+
+  public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.ExpandContainersSplitType();
+  }
+
+  public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.HingeSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+    method public androidx.window.extensions.embedding.SplitAttributes.SplitType getFallbackSplitType();
+  }
+
+  public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.RatioSplitType(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float);
+    method @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) public float getRatio();
+    method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+  }
+
+  public interface SplitAttributesCalculator {
+    method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
+  }
+
+  public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+    method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public android.view.WindowMetrics getParentWindowMetrics();
+    method public String? getSplitRuleTag();
+    method public boolean isDefaultMinSizeSatisfied();
   }
 
   public class SplitInfo {
-    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
-    method public float getSplitRatio();
+    method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
+    method @Deprecated public float getSplitRatio();
   }
 
   public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
@@ -61,13 +136,15 @@
   public static final class SplitPairRule.Builder {
     ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
     method public androidx.window.extensions.embedding.SplitPairRule build();
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
-    method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
     method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
     method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
-    method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
   }
 
   public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
@@ -82,17 +159,20 @@
   public static final class SplitPlaceholderRule.Builder {
     ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int);
     method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
-    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
-    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setTag(String);
   }
 
   public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
-    method public int getLayoutDirection();
-    method public float getSplitRatio();
+    method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+    method @Deprecated public int getLayoutDirection();
+    method @Deprecated public float getSplitRatio();
     field public static final int FINISH_ADJACENT = 2; // 0x2
     field public static final int FINISH_ALWAYS = 1; // 0x1
     field public static final int FINISH_NEVER = 0; // 0x0
@@ -119,6 +199,7 @@
 
   public interface WindowLayoutComponent {
     method public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+    method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
   }
 
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 8867f5f6c..449d9aa 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -4,6 +4,7 @@
   public interface WindowExtensions {
     method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
+    method public default androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
 
@@ -13,12 +14,30 @@
 
 }
 
+package androidx.window.extensions.area {
+
+  public interface WindowAreaComponent {
+    method public void addRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void endRearDisplaySession();
+    method public void removeRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void startRearDisplaySession(android.app.Activity, java.util.function.Consumer<java.lang.Integer!>);
+    field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+    field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+    field public static final int STATUS_AVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+  }
+
+}
+
 package androidx.window.extensions.embedding {
 
   public interface ActivityEmbeddingComponent {
+    method public void clearSplitAttributesCalculator();
     method public void clearSplitInfoCallback();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+    method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
     method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
 
@@ -32,6 +51,7 @@
     ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
     method public androidx.window.extensions.embedding.ActivityRule build();
     method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+    method public androidx.window.extensions.embedding.ActivityRule.Builder setTag(String);
   }
 
   public class ActivityStack {
@@ -41,13 +61,68 @@
   }
 
   public abstract class EmbeddingRule {
+    method public String? getTag();
+  }
+
+  public class SplitAttributes {
+    method @ColorInt public int getAnimationBackgroundColor();
+    method public int getLayoutDirection();
+    method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
+  }
+
+  public static final class SplitAttributes.Builder {
+    ctor public SplitAttributes.Builder();
+    method public androidx.window.extensions.embedding.SplitAttributes build();
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
+    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);
+  }
+
+  public static final class SplitAttributes.LayoutDirection {
+    field public static final int BOTTOM_TO_TOP = 5; // 0x5
+    field public static final int LEFT_TO_RIGHT = 0; // 0x0
+    field public static final int LOCALE = 3; // 0x3
+    field public static final int RIGHT_TO_LEFT = 1; // 0x1
+    field public static final int TOP_TO_BOTTOM = 4; // 0x4
+  }
+
+  public static class SplitAttributes.SplitType {
+  }
+
+  public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.ExpandContainersSplitType();
+  }
+
+  public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.HingeSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+    method public androidx.window.extensions.embedding.SplitAttributes.SplitType getFallbackSplitType();
+  }
+
+  public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.RatioSplitType(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float);
+    method @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) public float getRatio();
+    method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+  }
+
+  public interface SplitAttributesCalculator {
+    method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
+  }
+
+  public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+    method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public android.view.WindowMetrics getParentWindowMetrics();
+    method public String? getSplitRuleTag();
+    method public boolean isDefaultMinSizeSatisfied();
   }
 
   public class SplitInfo {
-    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
-    method public float getSplitRatio();
+    method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
+    method @Deprecated public float getSplitRatio();
   }
 
   public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
@@ -61,13 +136,15 @@
   public static final class SplitPairRule.Builder {
     ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
     method public androidx.window.extensions.embedding.SplitPairRule build();
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
-    method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
     method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
     method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
-    method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
   }
 
   public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
@@ -82,17 +159,20 @@
   public static final class SplitPlaceholderRule.Builder {
     ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int);
     method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
-    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
-    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setTag(String);
   }
 
   public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
-    method public int getLayoutDirection();
-    method public float getSplitRatio();
+    method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+    method @Deprecated public int getLayoutDirection();
+    method @Deprecated public float getSplitRatio();
     field public static final int FINISH_ADJACENT = 2; // 0x2
     field public static final int FINISH_ALWAYS = 1; // 0x1
     field public static final int FINISH_NEVER = 0; // 0x0
@@ -119,6 +199,7 @@
 
   public interface WindowLayoutComponent {
     method public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+    method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
   }
 
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 8867f5f6c..449d9aa 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -4,6 +4,7 @@
   public interface WindowExtensions {
     method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
+    method public default androidx.window.extensions.area.WindowAreaComponent? getWindowAreaComponent();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
 
@@ -13,12 +14,30 @@
 
 }
 
+package androidx.window.extensions.area {
+
+  public interface WindowAreaComponent {
+    method public void addRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void endRearDisplaySession();
+    method public void removeRearDisplayStatusListener(java.util.function.Consumer<java.lang.Integer!>);
+    method public void startRearDisplaySession(android.app.Activity, java.util.function.Consumer<java.lang.Integer!>);
+    field public static final int SESSION_STATE_ACTIVE = 1; // 0x1
+    field public static final int SESSION_STATE_INACTIVE = 0; // 0x0
+    field public static final int STATUS_AVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNSUPPORTED = 0; // 0x0
+  }
+
+}
+
 package androidx.window.extensions.embedding {
 
   public interface ActivityEmbeddingComponent {
+    method public void clearSplitAttributesCalculator();
     method public void clearSplitInfoCallback();
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+    method public void setSplitAttributesCalculator(androidx.window.extensions.embedding.SplitAttributesCalculator);
     method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
 
@@ -32,6 +51,7 @@
     ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
     method public androidx.window.extensions.embedding.ActivityRule build();
     method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+    method public androidx.window.extensions.embedding.ActivityRule.Builder setTag(String);
   }
 
   public class ActivityStack {
@@ -41,13 +61,68 @@
   }
 
   public abstract class EmbeddingRule {
+    method public String? getTag();
+  }
+
+  public class SplitAttributes {
+    method @ColorInt public int getAnimationBackgroundColor();
+    method public int getLayoutDirection();
+    method public androidx.window.extensions.embedding.SplitAttributes.SplitType getSplitType();
+  }
+
+  public static final class SplitAttributes.Builder {
+    ctor public SplitAttributes.Builder();
+    method public androidx.window.extensions.embedding.SplitAttributes build();
+    method public androidx.window.extensions.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int);
+    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);
+  }
+
+  public static final class SplitAttributes.LayoutDirection {
+    field public static final int BOTTOM_TO_TOP = 5; // 0x5
+    field public static final int LEFT_TO_RIGHT = 0; // 0x0
+    field public static final int LOCALE = 3; // 0x3
+    field public static final int RIGHT_TO_LEFT = 1; // 0x1
+    field public static final int TOP_TO_BOTTOM = 4; // 0x4
+  }
+
+  public static class SplitAttributes.SplitType {
+  }
+
+  public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.ExpandContainersSplitType();
+  }
+
+  public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.HingeSplitType(androidx.window.extensions.embedding.SplitAttributes.SplitType);
+    method public androidx.window.extensions.embedding.SplitAttributes.SplitType getFallbackSplitType();
+  }
+
+  public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.extensions.embedding.SplitAttributes.SplitType {
+    ctor public SplitAttributes.SplitType.RatioSplitType(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float);
+    method @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) public float getRatio();
+    method public static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+  }
+
+  public interface SplitAttributesCalculator {
+    method public androidx.window.extensions.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams);
+  }
+
+  public static class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+    method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.extensions.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public android.view.WindowMetrics getParentWindowMetrics();
+    method public String? getSplitRuleTag();
+    method public boolean isDefaultMinSizeSatisfied();
   }
 
   public class SplitInfo {
-    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
-    method public float getSplitRatio();
+    method public androidx.window.extensions.embedding.SplitAttributes getSplitAttributes();
+    method @Deprecated public float getSplitRatio();
   }
 
   public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
@@ -61,13 +136,15 @@
   public static final class SplitPairRule.Builder {
     ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
     method public androidx.window.extensions.embedding.SplitPairRule build();
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
-    method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
     method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
     method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
-    method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setTag(String);
   }
 
   public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
@@ -82,17 +159,20 @@
   public static final class SplitPlaceholderRule.Builder {
     ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.extensions.embedding.SplitAttributes);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int);
     method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
-    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
-    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float);
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setTag(String);
   }
 
   public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
-    method public int getLayoutDirection();
-    method public float getSplitRatio();
+    method public androidx.window.extensions.embedding.SplitAttributes getDefaultSplitAttributes();
+    method @Deprecated public int getLayoutDirection();
+    method @Deprecated public float getSplitRatio();
     field public static final int FINISH_ADJACENT = 2; // 0x2
     field public static final int FINISH_ALWAYS = 1; // 0x1
     field public static final int FINISH_NEVER = 0; // 0x0
@@ -119,6 +199,7 @@
 
   public interface WindowLayoutComponent {
     method public void addWindowLayoutInfoListener(android.app.Activity, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
+    method public default void addWindowLayoutInfoListener(@UiContext android.content.Context, java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
     method public void removeWindowLayoutInfoListener(java.util.function.Consumer<androidx.window.extensions.layout.WindowLayoutInfo!>);
   }
 
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index f9deca6..08c9424 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -24,9 +24,13 @@
 
 dependencies {
     api(libs.kotlinStdlib)
-    implementation("androidx.annotation:annotation:1.1.0")
+    implementation("androidx.annotation:annotation:1.3.0")
     implementation("androidx.annotation:annotation-experimental:1.1.0")
 
+    testImplementation(libs.testExtJunit)
+    testImplementation(libs.testRunner)
+    testImplementation(libs.testRules)
+
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index c1a9bd8..f8707a9 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -16,8 +16,13 @@
 
 package androidx.window.extensions;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.WindowAreaComponent;
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.embedding.SplitPlaceholderRule;
 import androidx.window.extensions.layout.WindowLayoutComponent;
 
 /**
@@ -29,6 +34,53 @@
  * {@link WindowExtensions#getVendorApiLevel()}.
  */
 public interface WindowExtensions {
+    // TODO(b/241323716) Removed after we have annotation to check API level
+    /**
+     * An invalid {@link #getVendorApiLevel vendor API level}
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    int INVALID_VENDOR_API_LEVEL = -1;
+
+    // TODO(b/241323716) Removed after we have annotation to check API level
+    /**
+     * A vendor API level constant. It helps to unify the format of documenting {@code @since}
+     * block.
+     * <p>
+     * The added APIs for Vendor API level 1 are:
+     * <ul>
+     *     <li>{@link androidx.window.extensions.embedding.ActivityRule} APIs</li>
+     *     <li>{@link androidx.window.extensions.embedding.SplitPairRule} APIs</li>
+     *     <li>{@link androidx.window.extensions.embedding.SplitPlaceholderRule} APIs</li>
+     *     <li>{@link androidx.window.extensions.embedding.SplitInfo} APIs</li>
+     *     <li>{@link androidx.window.extensions.layout.DisplayFeature} APIs</li>
+     *     <li>{@link androidx.window.extensions.layout.FoldingFeature} APIs</li>
+     *     <li>{@link androidx.window.extensions.layout.WindowLayoutInfo} APIs</li>
+     *     <li>{@link androidx.window.extensions.layout.WindowLayoutComponent} APIs</li>
+     *     <li>{@link androidx.window.extensions.area.WindowAreaComponent} APIs</li>
+     * </ul>
+     * </p>
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    int VENDOR_API_LEVEL_1 = 1;
+    // TODO(b/241323716) Removed after we have annotation to check API level
+    /**
+     * A vendor API level constant. It helps to unify the format of documenting {@code @since}
+     * block.
+     * <p>
+     * The added APIs for Vendor API level 2 are:
+     * <ul>
+     *     <li>{@link SplitPlaceholderRule.Builder#setFinishPrimaryWithPlaceholder(int)}</li>
+     *     <li>{@link androidx.window.extensions.embedding.SplitAttributes} APIs</li>
+     *     <li>{@link androidx.window.extensions.embedding.SplitAttributesCalculator} APIs</li>
+     * </ul>
+     * </p>
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    int VENDOR_API_LEVEL_2 = 2;
+
     /**
      * Returns the API level of the vendor library on the device. If the returned version is not
      * supported by the WindowManager library, then some functions may not be available or replaced
@@ -39,7 +91,7 @@
      * @return the API level supported by the library.
      */
     default int getVendorApiLevel() {
-        return 1;
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
     }
 
     /**
@@ -61,4 +113,15 @@
     default ActivityEmbeddingComponent getActivityEmbeddingComponent() {
         return null;
     }
+
+    /**
+     * Returns the OEM implementation of {@link WindowAreaComponent} if it is supported on
+     * the device, {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions}.
+     * @return the OEM implementation of {@link WindowAreaComponent}
+     */
+    @Nullable
+    default WindowAreaComponent getWindowAreaComponent() {
+        return null;
+    }
 }
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
new file mode 100644
index 0000000..afd3e36
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/area/WindowAreaComponent.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 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.window.extensions.area;
+
+import android.app.Activity;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.WindowExtensions;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.function.Consumer;
+
+/**
+ * The interface definition that will be used by the WindowManager library to get custom
+ * OEM-provided behavior around moving windows between displays or display areas on a device.
+ *
+ * Currently the only behavior supported is RearDisplay Mode, where the window
+ * is moved to the display that faces the same direction as the rear camera.
+ *
+ * <p>This interface should be implemented by OEM and deployed to the target devices.
+ * @see WindowExtensions#getWindowLayoutComponent()
+ */
+public interface WindowAreaComponent {
+
+    /**
+     * WindowArea status constant to signify that the feature is
+     * unsupported on this device. Could be due to the device not supporting that
+     * specific feature.
+     */
+    int STATUS_UNSUPPORTED = 0;
+
+    /**
+     * WindowArea status constant to signify that the feature is
+     * currently unavailable but is supported on this device. This value could signify
+     * that the current device state does not support the specific feature or another
+     * process is currently enabled in that feature.
+     */
+    int STATUS_UNAVAILABLE = 1;
+
+    /**
+     * WindowArea status constant to signify that the feature is
+     * available to be entered or enabled.
+     */
+    int STATUS_AVAILABLE = 2;
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    @IntDef({
+            STATUS_UNSUPPORTED,
+            STATUS_UNAVAILABLE,
+            STATUS_AVAILABLE
+    })
+    @interface WindowAreaStatus {}
+
+    /**
+     * Session state constant to represent there being no active session
+     * currently in progress. Used by the library to call the correct callbacks if
+     * a session is ended.
+     */
+    int SESSION_STATE_INACTIVE = 0;
+
+    /**
+     * Session state constant to represent that there is an
+     * active session currently in progress. Used by the library to
+     * know when to return the session object to the developer when the
+     * session is created and active.
+     */
+    int SESSION_STATE_ACTIVE = 1;
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    @IntDef({
+            SESSION_STATE_ACTIVE,
+            SESSION_STATE_INACTIVE
+    })
+    @interface WindowAreaSessionState {}
+
+    /**
+     * Adds a listener interested in receiving updates on the RearDisplayStatus
+     * of the device. Because this is being called from the OEM provided
+     * extensions, the library will post the result of the listener on the executor
+     * provided by the developer.
+     *
+     * The listener provided will receive values that
+     * correspond to the [WindowAreaStatus] value that aligns with the current status
+     * of the rear display.
+     * @param consumer interested in receiving updates to WindowAreaStatus.
+     */
+    void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+    /**
+     * Removes a listener no longer interested in receiving updates.
+     * @param consumer no longer interested in receiving updates to WindowAreaStatus
+     */
+    void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+
+    /**
+     * Creates and starts a rear display session and sends state updates to the
+     * consumer provided. This consumer will receive a constant represented by
+     * [WindowAreaSessionState] to represent the state of the current rear display
+     * session. We will translate to a more friendly interface in the library.
+     *
+     * Because this is being called from the OEM provided extensions, the library
+     * will post the result of the listener on the executor provided by the developer.
+     *
+     * @param activity to allow that the OEM implementation will use as a base
+     * context and to identify the source display area of the request.
+     * The reference to the activity instance must not be stored in the OEM
+     * implementation to prevent memory leaks.
+     * @param consumer to provide updates to the client on the status of the session
+     * @throws UnsupportedOperationException if this method is called when RearDisplay
+     * mode is not available. This could be to an incompatible device state or when
+     * another process is currently in this mode.
+     */
+    @SuppressWarnings("ExecutorRegistration") // Jetpack will post it on the app-provided executor.
+    void startRearDisplaySession(@NonNull Activity activity,
+            @NonNull Consumer<Integer> consumer);
+
+    /**
+     * Ends a RearDisplaySession and sends [STATE_INACTIVE] to the consumer
+     * provided in the {@code startRearDisplaySession} method. This method is only
+     * called through the {@code RearDisplaySession} provided to the developer.
+     */
+    void endRearDisplaySession();
+}
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 16caa61..2fbbd25 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
@@ -62,4 +62,21 @@
      * occupies a portion of Task bounds.
      */
     boolean isActivityEmbedded(@NonNull Activity activity);
+
+    /**
+     * Sets a {@link SplitAttributesCalculator}.
+     *
+     * @param calculator the calculator to set. It will replace the previously set
+     * {@link SplitAttributesCalculator} if it exists.
+     * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    void setSplitAttributesCalculator(@NonNull SplitAttributesCalculator calculator);
+
+    /**
+     * Clears the previously set {@link SplitAttributesCalculator}.
+     *
+     * @see #setSplitAttributesCalculator(SplitAttributesCalculator)
+     * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    void clearSplitAttributesCalculator();
 }
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 2d1fc33..c8bcd85 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
@@ -22,8 +22,10 @@
 import android.os.Build;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -37,7 +39,9 @@
     private final boolean mShouldAlwaysExpand;
 
     ActivityRule(@NonNull Predicate<Activity> activityPredicate,
-            @NonNull Predicate<Intent> intentPredicate, boolean shouldAlwaysExpand) {
+            @NonNull Predicate<Intent> intentPredicate, boolean shouldAlwaysExpand,
+            @Nullable String tag) {
+        super(tag);
         mActivityPredicate = activityPredicate;
         mIntentPredicate = intentPredicate;
         mShouldAlwaysExpand = shouldAlwaysExpand;
@@ -78,6 +82,8 @@
         @NonNull
         private final Predicate<Intent> mIntentPredicate;
         private boolean mAlwaysExpand;
+        @Nullable
+        private String mTag;
 
         public Builder(@NonNull Predicate<Activity> activityPredicate,
                 @NonNull Predicate<Intent> intentPredicate) {
@@ -92,10 +98,20 @@
             return this;
         }
 
+        /**
+         * @see ActivityRule#getTag()
+         * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+         */
+        @NonNull
+        public Builder setTag(@NonNull String tag) {
+            mTag = Objects.requireNonNull(tag);
+            return this;
+        }
+
         /** Builds a new instance of {@link ActivityRule}. */
         @NonNull
         public ActivityRule build() {
-            return new ActivityRule(mActivityPredicate, mIntentPredicate, mAlwaysExpand);
+            return new ActivityRule(mActivityPredicate, mIntentPredicate, mAlwaysExpand, mTag);
         }
     }
 
@@ -104,14 +120,16 @@
         if (this == o) return true;
         if (!(o instanceof ActivityRule)) return false;
         ActivityRule that = (ActivityRule) o;
-        return mShouldAlwaysExpand == that.mShouldAlwaysExpand
+        return super.equals(o)
+                && mShouldAlwaysExpand == that.mShouldAlwaysExpand
                 && mActivityPredicate.equals(that.mActivityPredicate)
                 && mIntentPredicate.equals(that.mIntentPredicate);
     }
 
     @Override
     public int hashCode() {
-        int result = mActivityPredicate.hashCode();
+        int result = super.hashCode();
+        result = 31 * result + mActivityPredicate.hashCode();
         result = 31 * result + mIntentPredicate.hashCode();
         result = 31 * result + (mShouldAlwaysExpand ? 1 : 0);
         return result;
@@ -120,6 +138,7 @@
     @NonNull
     @Override
     public String toString() {
-        return "ActivityRule{" + "mShouldAlwaysExpand=" + mShouldAlwaysExpand + '}';
+        return "ActivityRule{mTag=" + getTag()
+                + "mShouldAlwaysExpand=" + mShouldAlwaysExpand + '}';
     }
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
index 571eda0..179afb4 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
@@ -16,10 +16,47 @@
 
 package androidx.window.extensions.embedding;
 
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
 /**
  * Base interface for activity embedding rules. Used to group different types of rules together when
  * updating from the core library.
  */
 public abstract class EmbeddingRule {
-    EmbeddingRule() {}
+    @Nullable
+    private final String mTag;
+
+    EmbeddingRule(@Nullable String tag) {
+        mTag = tag;
+    }
+
+    // TODO(b/240912390): refer to the real API in later CLs.
+    /**
+     * A unique string to identify this {@link EmbeddingRule}.
+     * The suggested usage is to set the tag in the corresponding rule builder to be able to
+     * differentiate between different rules in the callbacks. For example, it can be used to
+     * compute the right {@link SplitAttributes} for the right split rule in
+     * {@code SplitAttributesCalculator#computeSplitAttributesForState}.
+     *
+     * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    @Nullable
+    public String getTag() {
+        return mTag;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) return true;
+        if (!(other instanceof EmbeddingRule)) return false;
+        final EmbeddingRule otherRule = (EmbeddingRule) other;
+        return Objects.equals(mTag, otherRule.mTag);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mTag);
+    }
 }
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
new file mode 100644
index 0000000..992d934
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributes.java
@@ -0,0 +1,560 @@
+/*
+ * 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.window.extensions.embedding;
+
+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 android.annotation.SuppressLint;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.layout.FoldingFeature;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Attributes that describe how the parent window (typically the activity task
+ * window) is split between the primary and secondary activity containers,
+ * including:
+ * <ul>
+ *     <li>Split type -- Categorizes the split and specifies the sizes of the
+ *         primary and secondary activity containers relative to the parent
+ *         bounds</li>
+ *     <li>Layout direction -- Specifies whether the parent window is split
+ *         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)</li>
+ *     <li>Animation background color -- The color of the background during
+ *         animation of the split involving this {@code SplitAttributes} object
+ *         if the animation requires a background</li>
+ * </ul>
+ *
+ * <p>Attributes can be configured by:
+ * <ul>
+ *     <li>Setting the default {@code SplitAttributes} using
+ *         {@link SplitPairRule.Builder#setDefaultSplitAttributes} or
+ *         {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes}.</li>
+ *     <li>Using
+ *         {@link SplitAttributesCalculator#computeSplitAttributesForParams}
+ *         to customize the {@code SplitAttributes} for a given device and
+ *         window state.</li>
+ * </ul>
+ *
+ * @see SplitAttributes.SplitType
+ * @see SplitAttributes.LayoutDirection
+ * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+ */
+public class SplitAttributes {
+    /**
+     * The type of window split, which defines the proportion of the parent
+     * window occupied by the primary and secondary activity containers.
+     */
+    public static class SplitType {
+        @NonNull
+        private final String mDescription;
+
+        SplitType(@NonNull String description) {
+            mDescription = description;
+        }
+
+        @Override
+        public int hashCode() {
+            return mDescription.hashCode();
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof SplitType)) {
+                return false;
+            }
+            final SplitType that = (SplitType) obj;
+            return mDescription.equals(that.mDescription);
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return mDescription;
+        }
+
+        @SuppressLint("Range") // The range is covered.
+        @NonNull
+        static SplitType createSplitTypeFromLegacySplitRatio(
+                @FloatRange(from = 0.0, to = 1.0) float splitRatio) {
+            // Treat 0.0 and 1.0 as ExpandContainerSplitType because it means the parent container
+            // is filled with secondary or primary container.
+            if (splitRatio == 0.0 || splitRatio == 1.0) {
+                return new ExpandContainersSplitType();
+            }
+            return new RatioSplitType(splitRatio);
+        }
+
+        /**
+         * A window split that's based on the ratio of the size of the primary
+         * container to the size of the parent window.
+         *
+         * <p>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:
+         * <ul>
+         *     <li>0.5 -- Primary container occupies half of the parent
+         *         window; secondary container, the other half</li>
+         *     <li>Greater than 0.5 -- Primary container occupies a larger
+         *         proportion of the parent window than the secondary
+         *         container</li>
+         *     <li>Less than 0.5 -- Primary container occupies a smaller
+         *         proportion of the parent window than the secondary
+         *         container</li>
+         * </ul>
+         */
+        public static final class RatioSplitType extends SplitType {
+            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+            private final float mRatio;
+
+            /**
+             * 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.
+             */
+            public RatioSplitType(
+                    @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+                    float ratio) {
+                super("ratio:" + ratio);
+                if (ratio <= 0.0f || ratio >= 1.0f) {
+                    throw new IllegalArgumentException("Ratio must be in range (0.0, 1.0). "
+                            + " Use SplitType.ExpandContainersSplitType() instead of 0 or 1.");
+                }
+                mRatio = ratio;
+            }
+
+            /**
+             * Gets the proportion of the parent window occupied by the primary
+             * activity container of the split.
+             *
+             * @return The proportion of the split occupied by the primary
+             *     container.
+             */
+            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+            public float getRatio() {
+                return mRatio;
+            }
+
+            /**
+             * Creates a split type in which the primary and secondary
+             * containers occupy equal portions of the parent window.
+             *
+             * Serves as the default {@link SplitType} if
+             * {@link SplitAttributes.Builder#setSplitType(SplitType)} is not
+             * specified.
+             *
+             * @return A {@code RatioSplitType} in which the activity containers
+             *     occupy equal portions of the parent window.
+             */
+            @NonNull
+            public static RatioSplitType splitEqually() {
+                return new RatioSplitType(0.5f);
+            }
+        }
+
+        /**
+         * A parent window split in which the split ratio conforms to the
+         * position of a hinge or separating fold in the device display.
+         *
+         * The split type is created only if:
+         * <ul>
+         *     <li>The host task is not in multi-window mode (e.g.,
+         *         split-screen mode or picture-in-picture mode)</li>
+         *     <li>The device has a hinge or separating fold reported by
+         *         [androidx.window.layout.FoldingFeature.isSeparating]</li>
+         *     <li>The hinge or separating fold orientation matches how the
+         *         parent bounds are split:
+         *         <ul>
+         *             <li>The hinge or fold orientation is vertical, and
+         *                 the task bounds are also split vertically
+         *                 (containers are side by side)</li>
+         *             <li>The hinge or fold orientation is horizontal, and
+         *                 the task bounds are also split horizontally
+         *                 (containers are top and bottom)</li>
+         *         </ul>
+         *     </li>
+         * </ul>
+         *
+         * Otherwise, the type falls back to the {@code SplitType} returned by
+         * {@link #getFallbackSplitType()}.
+         */
+        public static final class HingeSplitType extends SplitType {
+            @NonNull
+            private final SplitType mFallbackSplitType;
+
+            /**
+             * 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}.
+             */
+            public HingeSplitType(@NonNull SplitType fallbackSplitType) {
+                super("hinge, fallbackType=" + fallbackSplitType);
+                mFallbackSplitType = fallbackSplitType;
+            }
+
+            /**
+             * Returns the fallback {@link SplitType} if a split based on the
+             * device hinge or separating fold cannot be determined.
+             */
+            @NonNull
+            public SplitType getFallbackSplitType() {
+                return mFallbackSplitType;
+            }
+        }
+
+        /**
+         * A window split in which the primary and secondary activity containers
+         * each occupy the entire parent window.
+         *
+         * The secondary container overlays the primary container.
+         */
+        public static final class ExpandContainersSplitType extends SplitType {
+
+            /**
+             * Creates an instance of this {@code ExpandContainersSplitType}.
+             */
+            public ExpandContainersSplitType() {
+                super("expandContainers");
+            }
+        }
+    }
+
+    /**
+     * The layout direction of the primary and secondary activity containers.
+     */
+    public static final class LayoutDirection {
+
+        /**
+         * Specifies that the parent bounds are split vertically (side to side).
+         *
+         * Places the primary container in the left portion of the parent
+         * window, and the secondary container in the right portion.
+         *
+         * A possible return value of {@link SplitType#getLayoutDirection()}.
+         */
+         //
+         // -------------------------
+         // |           |           |
+         // |  Primary  | Secondary |
+         // |           |           |
+         // -------------------------
+         //
+         // Must match {@link LayoutDirection#LTR} for backwards compatibility
+         // with prior versions of Extensions.
+        public static final int LEFT_TO_RIGHT = 0;
+
+        /**
+         * Specifies that the parent bounds are split vertically (side to
+         * side).
+         *
+         * Places the primary container in the right portion of the parent
+         * window, and the secondary container in the left portion.
+         *
+         * A possible return value of {@link SplitType#getLayoutDirection()}.
+         */
+         // -------------------------
+         // |           |           |
+         // | Secondary |  Primary  |
+         // |           |           |
+         // -------------------------
+         //
+         // Must match {@link LayoutDirection#RTL} for backwards compatibility
+         // with prior versions of Extensions.
+        public static final int RIGHT_TO_LEFT = 1;
+
+        /**
+         * Specifies that the parent bounds are split vertically (side to side).
+         *
+         * The direction of the primary and secondary containers is deduced from
+         * the locale as either {@link #LEFT_TO_RIGHT} or
+         * {@link #RIGHT_TO_LEFT}.
+         *
+         * A possible return value of {@link SplitType#getLayoutDirection()}.
+         */
+         // Must match {@link LayoutDirection#LOCALE} for backwards
+         // compatibility with prior versions of Extensions.
+        public static final int LOCALE = 3;
+
+        /**
+         * Specifies that the parent bounds are split horizontally (top and
+         * bottom).
+         *
+         * Places the primary container in the top portion of the parent window,
+         * and the secondary container in the bottom portion.
+         *
+         * If the horizontal layout direction is not supported on the device,
+         * layout direction falls back to {@link #LOCALE}.
+         *
+         * A possible return value of {@link SplitType#getLayoutDirection()}.
+         */
+         // -------------
+         // |           |
+         // |  Primary  |
+         // |           |
+         // -------------
+         // |           |
+         // | Secondary |
+         // |           |
+         // -------------
+        public static final int TOP_TO_BOTTOM = 4;
+
+        /**
+         * Specifies that the parent bounds are split horizontally (top and
+         * bottom).
+         *
+         * Places the primary container in the bottom portion of the parent
+         * window, and the secondary container in the top portion.
+         *
+         * If the horizontal layout direction is not supported on the device,
+         * layout direction falls back to {@link #LOCALE}.
+         *
+         * A possible return value of {@link SplitType#getLayoutDirection()}.
+         */
+         // -------------
+         // |           |
+         // | Secondary |
+         // |           |
+         // -------------
+         // |           |
+         // |  Primary  |
+         // |           |
+         // -------------
+        public static final int BOTTOM_TO_TOP = 5;
+
+        private LayoutDirection() {}
+    }
+
+    @IntDef({LEFT_TO_RIGHT, RIGHT_TO_LEFT, LOCALE, TOP_TO_BOTTOM, BOTTOM_TO_TOP})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ExtLayoutDirection {}
+
+    @ExtLayoutDirection
+    private final int mLayoutDirection;
+
+    private final SplitType mSplitType;
+
+    @ColorInt
+    private final int mAnimationBackgroundColor;
+
+    /**
+     * 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.
+     */
+    SplitAttributes(@NonNull SplitType splitType, @ExtLayoutDirection int layoutDirection,
+            @ColorInt int animationBackgroundColor) {
+        mSplitType = splitType;
+        mLayoutDirection = layoutDirection;
+        mAnimationBackgroundColor = animationBackgroundColor;
+    }
+
+    /**
+     * Gets the layout direction of the split.
+     *
+     * @return The layout direction of the split.
+     */
+    @ExtLayoutDirection
+    public int getLayoutDirection() {
+        return mLayoutDirection;
+    }
+
+    /**
+     * Gets the split type.
+     *
+     * @return The split type.
+     */
+    @NonNull
+    public SplitType getSplitType() {
+        return mSplitType;
+    }
+
+    /**
+     * Gets the {@link ColorInt} to use for the background color during the
+     * animation of the split involving this {@code SplitAttributes} object.
+     *
+     * @return The animation background {@code ColorInt}.
+     */
+    @ColorInt
+    public int getAnimationBackgroundColor() {
+        return mAnimationBackgroundColor;
+    }
+
+    /**
+     * Builder for creating an instance of {@link SplitAttributes}.
+     *
+     * 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 0, which specifies the theme window
+     * background color.
+     */
+    public static final class Builder {
+        @NonNull
+        private SplitType mSplitType =  new SplitType.RatioSplitType(0.5f);
+        @ExtLayoutDirection
+        private int mLayoutDirection = LOCALE;
+
+        @ColorInt
+        private int mAnimationBackgroundColor = 0;
+
+        /**
+         * Sets the split type attribute.
+         *
+         * The default is an equal split between primary and secondary
+         * containers (see {@link SplitType.RatioSplitType#splitEqually()}).
+         *
+         * @param splitType The split type attribute.
+         * @return This {@code Builder}.
+         */
+        @NonNull
+        public Builder setSplitType(@NonNull SplitType splitType) {
+            mSplitType = splitType;
+            return this;
+        }
+
+        /**
+         * Sets the split layout direction attribute.
+         *
+         * The default is based on locale.
+         *
+         * Must be one of:
+         * <ul>
+         *     <li>{@link LayoutDirection#LOCALE}</li>
+         *     <li>{@link LayoutDirection#LEFT_TO_RIGHT}</li>
+         *     <li>{@link LayoutDirection#RIGHT_TO_LEFT}</li>
+         *     <li>{@link LayoutDirection#TOP_TO_BOTTOM}</li>
+         *     <li>{@link LayoutDirection#BOTTOM_TO_TOP}</li>
+         * </ul>
+         *
+         * @param layoutDirection The layout direction attribute.
+         * @return This {@code Builder}.
+         */
+        @SuppressLint("WrongConstant") // To be compat with android.util.LayoutDirection APIs
+        @NonNull
+        public Builder setLayoutDirection(@ExtLayoutDirection int layoutDirection) {
+            mLayoutDirection = layoutDirection;
+            return this;
+        }
+
+        /**
+         * Sets the {@link ColorInt} to use for the background during the
+         * animation of the split involving this {@code SplitAttributes} object
+         * if the animation requires a background.
+         *
+         * The default value is 0, which specifies the theme window background
+         * color.
+         *
+         * @param color A packed color int of the form {@code AARRGGBB} for the
+         *     animation background color.
+         * @return This {@code Builder}.
+         */
+        @NonNull
+        public Builder setAnimationBackgroundColor(@ColorInt int color) {
+            mAnimationBackgroundColor = color;
+            return this;
+        }
+
+        /**
+         * Builds a {@link SplitAttributes} instance with the attributes
+         * specified by {@link #setSplitType}, {@link #setLayoutDirection}, and
+         * {@link #setAnimationBackgroundColor}.
+         *
+         * @return The new {@code SplitAttributes} instance.
+         */
+        @NonNull
+        public SplitAttributes build() {
+            return new SplitAttributes(mSplitType, mLayoutDirection, mAnimationBackgroundColor);
+        }
+    }
+
+    @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;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return SplitAttributes.class.getSimpleName() + "{"
+                + "layoutDir=" + layoutDirectionToString()
+                + ", ratio=" + mSplitType
+                + ", animationBgColor=" + Integer.toHexString(mAnimationBackgroundColor)
+                + "}";
+    }
+
+    @NonNull
+    private String layoutDirectionToString() {
+        switch(mLayoutDirection) {
+            case LEFT_TO_RIGHT:
+                return "LEFT_TO_RIGHT";
+            case RIGHT_TO_LEFT:
+                return "RIGHT_TO_LEFT";
+            case LOCALE:
+                return "LOCALE";
+            case TOP_TO_BOTTOM:
+                return "TOP_TO_BOTTOM";
+            case BOTTOM_TO_TOP:
+                return "BOTTOM_TO_TOP";
+            default:
+                throw new IllegalArgumentException("Invalid layout direction:" + mLayoutDirection);
+        }
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
new file mode 100644
index 0000000..ef69323
--- /dev/null
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitAttributesCalculator.java
@@ -0,0 +1,156 @@
+/*
+ * 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.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.layout.WindowLayoutInfo;
+
+/**
+ * A developer-defined {@link SplitAttributes} calculator to compute the current split layout with
+ * the current device and window state.
+ *
+ * @see ActivityEmbeddingComponent#setSplitAttributesCalculator(SplitAttributesCalculator)
+ * @see ActivityEmbeddingComponent#clearSplitAttributesCalculator()
+ * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+ */
+public interface SplitAttributesCalculator {
+    /**
+     * Computes the {@link SplitAttributes} with the current device state.
+     *
+     * @param params See {@link SplitAttributesCalculatorParams}
+     */
+    @NonNull
+    SplitAttributes computeSplitAttributesForParams(
+            @NonNull SplitAttributesCalculatorParams params
+    );
+
+    /** The container of {@link SplitAttributesCalculator} parameters */
+    class SplitAttributesCalculatorParams {
+        @NonNull
+        private final WindowMetrics mParentWindowMetrics;
+        @NonNull
+        private final Configuration mParentConfiguration;
+        @NonNull
+        private final SplitAttributes mDefaultSplitAttributes;
+        private final boolean mIsDefaultMinSizeSatisfied;
+        @NonNull
+        private final WindowLayoutInfo mParentWindowLayoutInfo;
+        @Nullable
+        private final String mSplitRuleTag;
+
+        /** Returns the parent container's {@link WindowMetrics} */
+        @NonNull
+        public WindowMetrics getParentWindowMetrics() {
+            return mParentWindowMetrics;
+        }
+
+        /** Returns the parent container's {@link Configuration} */
+        @NonNull
+        public Configuration getParentConfiguration() {
+            return new Configuration(mParentConfiguration);
+        }
+
+        /**
+         * Returns the {@link SplitRule#getDefaultSplitAttributes()}. It could be from
+         * {@link SplitRule} Builder APIs
+         * ({@link SplitPairRule.Builder#setDefaultSplitAttributes(SplitAttributes)} or
+         * {@link SplitPlaceholderRule.Builder#setDefaultSplitAttributes(SplitAttributes)}) or from
+         * the {@code splitRatio} and {@code splitLayoutDirection} attributes from static rule
+         * definitions.
+         */
+        @NonNull
+        public SplitAttributes getDefaultSplitAttributes() {
+            return mDefaultSplitAttributes;
+        }
+
+        /**
+         * Returns whether the {@link #getParentWindowMetrics()} satisfies
+         * {@link SplitRule#checkParentMetrics(WindowMetrics)} with the minimal size requirement
+         * specified in the {@link SplitRule} Builder constructors.
+         *
+         * @see SplitPairRule.Builder
+         * @see SplitPlaceholderRule.Builder
+         */
+        public boolean isDefaultMinSizeSatisfied() {
+            return mIsDefaultMinSizeSatisfied;
+        }
+
+        /** Returns the parent container's {@link WindowLayoutInfo} */
+        @NonNull
+        public WindowLayoutInfo getParentWindowLayoutInfo() {
+            return mParentWindowLayoutInfo;
+        }
+
+        /** Returns {@link SplitRule#getTag()} to apply the {@link SplitAttributes} result. */
+        @Nullable
+        public String getSplitRuleTag() {
+            return mSplitRuleTag;
+        }
+
+        SplitAttributesCalculatorParams(
+                @NonNull WindowMetrics parentWindowMetrics,
+                @NonNull Configuration parentConfiguration,
+                @NonNull SplitAttributes defaultSplitAttributes,
+                boolean isDefaultMinSizeSatisfied,
+                @NonNull WindowLayoutInfo parentWindowLayoutInfo,
+                @Nullable String splitRuleTag
+        ) {
+            mParentWindowMetrics = parentWindowMetrics;
+            mParentConfiguration = parentConfiguration;
+            mDefaultSplitAttributes = defaultSplitAttributes;
+            mIsDefaultMinSizeSatisfied = isDefaultMinSizeSatisfied;
+            mParentWindowLayoutInfo = parentWindowLayoutInfo;
+            mSplitRuleTag = splitRuleTag;
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + ":{"
+                    + "windowMetrics=" + windowMetricsToString(mParentWindowMetrics)
+                    + ", configuration=" + mParentConfiguration
+                    + ", windowLayoutInfo=" + mParentWindowLayoutInfo
+                    + ", defaultSplitAttributes=" + mDefaultSplitAttributes
+                    + ", isDefaultMinSizeSatisfied=" + mIsDefaultMinSizeSatisfied
+                    + ", 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 cfd8b1a..4f28788 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
@@ -17,6 +17,8 @@
 package androidx.window.extensions.embedding;
 
 import androidx.annotation.NonNull;
+import androidx.window.extensions.WindowExtensions;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType;
 
 /** Describes a split of two containers with activities. */
 public class SplitInfo {
@@ -24,13 +26,16 @@
     private final ActivityStack mPrimaryActivityStack;
     @NonNull
     private final ActivityStack mSecondaryActivityStack;
-    private final float mSplitRatio;
+    @NonNull
+    private final SplitAttributes mSplitAttributes;
 
+    /** @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2} */
     public SplitInfo(@NonNull ActivityStack primaryActivityStack,
-            @NonNull ActivityStack secondaryActivityStack, float splitRatio) {
+            @NonNull ActivityStack secondaryActivityStack,
+            @NonNull SplitAttributes splitAttributes) {
         mPrimaryActivityStack = primaryActivityStack;
         mSecondaryActivityStack = secondaryActivityStack;
-        mSplitRatio = splitRatio;
+        mSplitAttributes = splitAttributes;
     }
 
     @NonNull
@@ -43,8 +48,28 @@
         return mSecondaryActivityStack;
     }
 
+    /**
+     * @deprecated Use {@link #getSplitAttributes()} starting with
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if {@link #getSplitAttributes()}
+     * can't be called on {@link WindowExtensions#VENDOR_API_LEVEL_1}.
+     */
+    @Deprecated
     public float getSplitRatio() {
-        return mSplitRatio;
+        final SplitType splitType = mSplitAttributes.getSplitType();
+        if (splitType instanceof SplitType.RatioSplitType) {
+            return ((SplitType.RatioSplitType) splitType).getRatio();
+        } else { // Fallback to use 0.0 because the WM Jetpack may not support HingeSplitType.
+            return 0.0f;
+        }
+    }
+
+    /**
+     * Returns the {@link SplitAttributes} of this split.
+     * @since {@link androidx.window.extensions.WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    @NonNull
+    public SplitAttributes getSplitAttributes() {
+        return mSplitAttributes;
     }
 
     @Override
@@ -52,7 +77,7 @@
         if (this == o) return true;
         if (!(o instanceof SplitInfo)) return false;
         SplitInfo that = (SplitInfo) o;
-        return Float.compare(that.mSplitRatio, mSplitRatio) == 0 && mPrimaryActivityStack.equals(
+        return mSplitAttributes.equals(that.mSplitAttributes) && mPrimaryActivityStack.equals(
                 that.mPrimaryActivityStack) && mSecondaryActivityStack.equals(
                 that.mSecondaryActivityStack);
     }
@@ -61,7 +86,7 @@
     public int hashCode() {
         int result = mPrimaryActivityStack.hashCode();
         result = result * 31 + mSecondaryActivityStack.hashCode();
-        result = result * 31 + (int) (mSplitRatio * 17);
+        result = result * 31 + mSplitAttributes.hashCode();
         return result;
     }
 
@@ -71,7 +96,7 @@
         return "SplitInfo{"
                 + "mPrimaryActivityStack=" + mPrimaryActivityStack
                 + ", mSecondaryActivityStack=" + mSecondaryActivityStack
-                + ", mSplitRatio=" + mSplitRatio
+                + ", mSplitAttributes=" + mSplitAttributes
                 + '}';
     }
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index d2e5209..1a75bf3 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -16,6 +16,8 @@
 
 package androidx.window.extensions.embedding;
 
+import static androidx.window.extensions.embedding.SplitAttributes.SplitType.createSplitTypeFromLegacySplitRatio;
+
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Intent;
@@ -23,9 +25,13 @@
 import android.util.Pair;
 import android.view.WindowMetrics;
 
+import androidx.annotation.FloatRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.window.extensions.WindowExtensions;
 
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -42,13 +48,14 @@
     private final int mFinishSecondaryWithPrimary;
     private final boolean mClearTop;
 
-    SplitPairRule(float splitRatio, @LayoutDirection int layoutDirection,
+    SplitPairRule(@NonNull SplitAttributes defaultSplitAttributes,
             @SplitFinishBehavior int finishPrimaryWithSecondary,
             @SplitFinishBehavior int finishSecondaryWithPrimary, boolean clearTop,
             @NonNull Predicate<Pair<Activity, Activity>> activityPairPredicate,
             @NonNull Predicate<Pair<Activity, Intent>> activityIntentPredicate,
-            @NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate) {
-        super(parentWindowMetricsPredicate, splitRatio, layoutDirection);
+            @NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate,
+            @Nullable String tag) {
+        super(parentWindowMetricsPredicate, defaultSplitAttributes, tag);
         mActivityPairPredicate = activityPairPredicate;
         mActivityIntentPredicate = activityIntentPredicate;
         mFinishPrimaryWithSecondary = finishPrimaryWithSecondary;
@@ -114,14 +121,20 @@
         private final Predicate<Pair<Activity, Intent>> mActivityIntentPredicate;
         @NonNull
         private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
+        // Keep for backward compatibility
+        @FloatRange(from = 0.0, to = 1.0)
         private float mSplitRatio;
-        @LayoutDirection
+        // Keep for backward compatibility
+        @SplitAttributes.ExtLayoutDirection
         private int mLayoutDirection;
+        private SplitAttributes mDefaultSplitAttributes;
         private boolean mClearTop;
         @SplitFinishBehavior
         private int mFinishPrimaryWithSecondary;
         @SplitFinishBehavior
         private int mFinishSecondaryWithPrimary;
+        @Nullable
+        private String mTag;
 
         public Builder(@NonNull Predicate<Pair<Activity, Activity>> activityPairPredicate,
                 @NonNull Predicate<Pair<Activity, Intent>> activityIntentPredicate,
@@ -131,20 +144,48 @@
             mParentWindowMetricsPredicate = parentWindowMetricsPredicate;
         }
 
-        /** @see SplitRule#getSplitRatio() */
+        /**
+         * @deprecated Use {@link #setDefaultSplitAttributes(SplitAttributes)} starting with
+         * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+         * {@link #setDefaultSplitAttributes(SplitAttributes)} can't be called on
+         * {@link WindowExtensions#VENDOR_API_LEVEL_1}. {@code splitRatio} will be translated to
+         * {@link SplitAttributes.SplitType.ExpandContainersSplitType} for value {@code 0.0} and
+         * {@code 1.0}, and {@link SplitAttributes.SplitType.RatioSplitType} for value with range
+         * (0.0, 1.0).
+         */
+        @Deprecated
         @NonNull
-        public Builder setSplitRatio(float splitRatio) {
+        public Builder setSplitRatio(@FloatRange(from = 0.0, to = 1.0) float splitRatio) {
             mSplitRatio = splitRatio;
             return this;
         }
 
-        /** @see SplitRule#getLayoutDirection() */
+        /**
+         * @deprecated Use {@link #setDefaultSplitAttributes(SplitAttributes)} starting with
+         * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+         * {@link #setDefaultSplitAttributes(SplitAttributes)} can't be called on
+         * {@link WindowExtensions#VENDOR_API_LEVEL_1}.
+         */
+        @Deprecated
         @NonNull
-        public Builder setLayoutDirection(@LayoutDirection int layoutDirection) {
+        public Builder setLayoutDirection(@SplitAttributes.ExtLayoutDirection int layoutDirection) {
             mLayoutDirection = layoutDirection;
             return this;
         }
 
+        /**
+         * See {@link SplitPairRule#getDefaultSplitAttributes()} for reference.
+         * Overrides values if set in {@link #setSplitRatio(float)} and
+         * {@link #setLayoutDirection(int)}
+         *
+         * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+         */
+        @NonNull
+        public Builder setDefaultSplitAttributes(@NonNull SplitAttributes attrs) {
+            mDefaultSplitAttributes = attrs;
+            return this;
+        }
+
         /** @deprecated To be removed with next developer preview. */
         @Deprecated
         @NonNull
@@ -181,13 +222,31 @@
             return this;
         }
 
+        /**
+         * @see SplitPairRule#getTag()
+         * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+         */
+        @NonNull
+        public Builder setTag(@NonNull String tag) {
+            mTag = Objects.requireNonNull(tag);
+            return this;
+        }
+
         /** Builds a new instance of {@link SplitPairRule}. */
         @NonNull
         public SplitPairRule build() {
-            return new SplitPairRule(mSplitRatio, mLayoutDirection,
+            // To provide compatibility with prior version of WM Jetpack library, where
+            // #setDefaultAttributes hasn't yet been supported and thus would not be set.
+            mDefaultSplitAttributes = (mDefaultSplitAttributes != null)
+                    ? mDefaultSplitAttributes
+                    : new SplitAttributes.Builder()
+                            .setSplitType(createSplitTypeFromLegacySplitRatio(mSplitRatio))
+                            .setLayoutDirection(mLayoutDirection)
+                            .build();
+            return new SplitPairRule(mDefaultSplitAttributes,
                     mFinishPrimaryWithSecondary, mFinishSecondaryWithPrimary,
                     mClearTop, mActivityPairPredicate, mActivityIntentPredicate,
-                    mParentWindowMetricsPredicate);
+                    mParentWindowMetricsPredicate, mTag);
         }
     }
 
@@ -219,7 +278,9 @@
     @Override
     public String toString() {
         return "SplitPairRule{"
-                + "mFinishPrimaryWithSecondary=" + mFinishPrimaryWithSecondary
+                + "mTag=" + getTag()
+                + ", mDefaultSplitAttributes=" + getDefaultSplitAttributes()
+                + ", mFinishPrimaryWithSecondary=" + mFinishPrimaryWithSecondary
                 + ", mFinishSecondaryWithPrimary=" + mFinishSecondaryWithPrimary
                 + ", mClearTop=" + mClearTop
                 + '}';
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 7b30849..0e61c5a 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
@@ -16,18 +16,24 @@
 
 package androidx.window.extensions.embedding;
 
+import static androidx.window.extensions.embedding.SplitAttributes.SplitType.createSplitTypeFromLegacySplitRatio;
+
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Build;
 import android.view.WindowMetrics;
 
+import androidx.annotation.FloatRange;
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.window.extensions.WindowExtensions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -58,12 +64,14 @@
     private final int mFinishPrimaryWithPlaceholder;
 
     SplitPlaceholderRule(@NonNull Intent placeholderIntent,
-            float splitRatio, @LayoutDirection int layoutDirection, boolean isSticky,
+            @NonNull SplitAttributes defaultSplitAttributes,
+            boolean isSticky,
             @SplitPlaceholderFinishBehavior int finishPrimaryWithPlaceholder,
             @NonNull Predicate<Activity> activityPredicate,
             @NonNull Predicate<Intent> intentPredicate,
-            @NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate) {
-        super(parentWindowMetricsPredicate, splitRatio, layoutDirection);
+            @NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate,
+            @Nullable String tag) {
+        super(parentWindowMetricsPredicate, defaultSplitAttributes, tag);
         mIsSticky = isSticky;
         mFinishPrimaryWithPlaceholder = finishPrimaryWithPlaceholder;
         mActivityPredicate = activityPredicate;
@@ -106,7 +114,10 @@
     }
 
     /**
-     * @deprecated Use {@link #getFinishPrimaryWithPlaceholder()} instead.
+     * @deprecated Use {@link #getFinishPrimaryWithPlaceholder()} instead starting with
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+     * {@link #getFinishPrimaryWithPlaceholder()} can't be called on
+     * {@link WindowExtensions#VENDOR_API_LEVEL_1}.
      */
     @Deprecated
     @SplitPlaceholderFinishBehavior
@@ -117,8 +128,10 @@
     /**
      * 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.
+     *
+     * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
      */
+    // TODO(b/238905747): Add api guard for extensions.
     @SplitPlaceholderFinishBehavior
     public int getFinishPrimaryWithPlaceholder() {
         return mFinishPrimaryWithPlaceholder;
@@ -136,12 +149,18 @@
         private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
         @NonNull
         private final Intent mPlaceholderIntent;
+        // Keep for backward compatibility
+        @FloatRange(from = 0.0, to = 1.0)
         private float mSplitRatio;
-        @LayoutDirection
+        // Keep for backward compatibility
+        @SplitAttributes.ExtLayoutDirection
         private int mLayoutDirection;
+        private SplitAttributes mDefaultSplitAttributes;
         private boolean mIsSticky = false;
         @SplitPlaceholderFinishBehavior
         private int mFinishPrimaryWithPlaceholder = FINISH_ALWAYS;
+        @Nullable
+        private String mTag;
 
         public Builder(@NonNull Intent placeholderIntent,
                 @NonNull Predicate<Activity> activityPredicate,
@@ -153,20 +172,48 @@
             mParentWindowMetricsPredicate = parentWindowMetricsPredicate;
         }
 
-        /** @see SplitRule#getSplitRatio() */
+        /**
+         * @deprecated Use {@link #setDefaultSplitAttributes(SplitAttributes)} starting with
+         * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+         * {@link #setDefaultSplitAttributes(SplitAttributes)} can't be called on
+         * {@link WindowExtensions#VENDOR_API_LEVEL_1}. {@code splitRatio} will be translated to
+         * @link SplitAttributes.SplitType.ExpandContainersSplitType} for value
+         * {@code 0.0} and {@code 1.0}, and {@link SplitAttributes.SplitType.RatioSplitType} for
+         * value with range (0.0, 1.0).
+         */
+        @Deprecated
         @NonNull
-        public Builder setSplitRatio(float splitRatio) {
+        public Builder setSplitRatio(@FloatRange(from = 0.0, to = 1.0) float splitRatio) {
             mSplitRatio = splitRatio;
             return this;
         }
 
-        /** @see SplitRule#getLayoutDirection() */
+        /**
+         * @deprecated Use {@link #setDefaultSplitAttributes(SplitAttributes)} starting with
+         * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+         * {@link #setDefaultSplitAttributes(SplitAttributes)} can't be called on
+         * {@link WindowExtensions#VENDOR_API_LEVEL_1}.
+         */
+        @Deprecated
         @NonNull
-        public Builder setLayoutDirection(@LayoutDirection int layoutDirection) {
+        public Builder setLayoutDirection(@SplitAttributes.ExtLayoutDirection int layoutDirection) {
             mLayoutDirection = layoutDirection;
             return this;
         }
 
+        /**
+         * See {@link SplitPlaceholderRule#getDefaultSplitAttributes()} for reference.
+         * Overrides values if set in {@link #setSplitRatio(float)} and
+         * {@link #setLayoutDirection(int)}
+         *
+         * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+         */
+        @NonNull
+        public Builder setDefaultSplitAttributes(@NonNull SplitAttributes attrs) {
+            mDefaultSplitAttributes = attrs;
+            return this;
+        }
+
         /** @see SplitPlaceholderRule#isSticky() */
         @NonNull
         public Builder setSticky(boolean sticky) {
@@ -175,7 +222,8 @@
         }
 
         /**
-         * @deprecated Use SplitPlaceholderRule#setFinishPrimaryWithPlaceholder(int)} instead.
+         * @deprecated Use SplitPlaceholderRule#setFinishPrimaryWithPlaceholder(int)} starting with
+         * {@link WindowExtensions#VENDOR_API_LEVEL_2}.
          */
         @Deprecated
         @NonNull
@@ -189,8 +237,9 @@
 
         /**
          * @see SplitPlaceholderRule#getFinishPrimaryWithPlaceholder()
-         * TODO(b/238905747): Add api guard for extensions.
+         * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
          */
+        // TODO(b/238905747): Add api guard for extensions.
         @NonNull
         public Builder setFinishPrimaryWithPlaceholder(
                 @SplitPlaceholderFinishBehavior int finishBehavior) {
@@ -198,12 +247,30 @@
             return this;
         }
 
+        /**
+         * @see SplitPlaceholderRule#getTag()
+         * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+         */
+        @NonNull
+        public Builder setTag(@NonNull String tag) {
+            mTag = Objects.requireNonNull(tag);
+            return this;
+        }
+
         /** Builds a new instance of {@link SplitPlaceholderRule}. */
         @NonNull
         public SplitPlaceholderRule build() {
-            return new SplitPlaceholderRule(mPlaceholderIntent, mSplitRatio,
-                    mLayoutDirection, mIsSticky, mFinishPrimaryWithPlaceholder, mActivityPredicate,
-                    mIntentPredicate, mParentWindowMetricsPredicate);
+            // To provide compatibility with prior version of WM Jetpack library, where
+            // #setDefaultAttributes hasn't yet been supported and thus would not be set.
+            mDefaultSplitAttributes = (mDefaultSplitAttributes != null)
+                    ? mDefaultSplitAttributes
+                    : new SplitAttributes.Builder()
+                            .setSplitType(createSplitTypeFromLegacySplitRatio(mSplitRatio))
+                            .setLayoutDirection(mLayoutDirection)
+                            .build();
+            return new SplitPlaceholderRule(mPlaceholderIntent, mDefaultSplitAttributes, mIsSticky,
+                    mFinishPrimaryWithPlaceholder, mActivityPredicate,
+                    mIntentPredicate, mParentWindowMetricsPredicate, mTag);
         }
     }
 
@@ -237,7 +304,9 @@
     @Override
     public String toString() {
         return "SplitPlaceholderRule{"
-                + "mActivityPredicate=" + mActivityPredicate
+                + "mTag=" + getTag()
+                + ", mDefaultSplitAttributes=" + getDefaultSplitAttributes()
+                + ", mActivityPredicate=" + mActivityPredicate
                 + ", mIsSticky=" + mIsSticky
                 + ", mFinishPrimaryWithPlaceholder=" + mFinishPrimaryWithPlaceholder
                 + '}';
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index bb24318..271b28e 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -16,20 +16,20 @@
 
 package androidx.window.extensions.embedding;
 
-import static android.util.LayoutDirection.LOCALE;
-import static android.util.LayoutDirection.LTR;
-import static android.util.LayoutDirection.RTL;
-
 import android.annotation.SuppressLint;
 import android.os.Build;
 import android.view.WindowMetrics;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.window.extensions.WindowExtensions;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -42,13 +42,10 @@
 public abstract class SplitRule extends EmbeddingRule {
     @NonNull
     private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
-    private final float mSplitRatio;
-    @LayoutDirection
-    private final int mLayoutDirection;
 
-    @IntDef({LTR, RTL, LOCALE})
-    @Retention(RetentionPolicy.SOURCE)
-    @interface LayoutDirection {}
+    @NonNull
+    private final SplitAttributes mDefaultSplitAttributes;
+
     /**
      * Never finish the associated container.
      * @see SplitFinishBehavior
@@ -88,29 +85,56 @@
     @Retention(RetentionPolicy.SOURCE)
     @interface SplitFinishBehavior {}
 
-    SplitRule(@NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate, float splitRatio,
-            @LayoutDirection int layoutDirection) {
+    SplitRule(@NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate,
+            @NonNull SplitAttributes defaultSplitAttributes, @Nullable String tag) {
+        super(tag);
         mParentWindowMetricsPredicate = parentWindowMetricsPredicate;
-        mSplitRatio = splitRatio;
-        mLayoutDirection = layoutDirection;
+        mDefaultSplitAttributes = defaultSplitAttributes;
     }
 
-    /**
-     * Verifies if the provided parent bounds allow to show the split containers side by side.
-     */
     @SuppressLint("ClassVerificationFailure") // Only called by Extensions implementation on device.
     @RequiresApi(api = Build.VERSION_CODES.N)
     public boolean checkParentMetrics(@NonNull WindowMetrics parentMetrics) {
         return mParentWindowMetricsPredicate.test(parentMetrics);
     }
 
+    /**
+     * @deprecated Use {@link #getDefaultSplitAttributes()} instead starting with
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+     * {@link #getDefaultSplitAttributes()} can't be called on
+     * {@link WindowExtensions#VENDOR_API_LEVEL_1}.
+     */
+    @Deprecated
     public float getSplitRatio() {
-        return mSplitRatio;
+        final SplitType splitType = mDefaultSplitAttributes.getSplitType();
+        if (splitType instanceof SplitType.RatioSplitType) {
+            return ((SplitType.RatioSplitType) splitType).getRatio();
+        } else { // Fallback to use 0.0 because the WM Jetpack may not support HingeSplitType.
+            return 0.0f;
+        }
     }
 
-    @LayoutDirection
+    /**
+     * @deprecated Use {@link #getDefaultSplitAttributes()} instead starting with
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}. Only used if
+     * {@link #getDefaultSplitAttributes()} can't be called on
+     * {@link WindowExtensions#VENDOR_API_LEVEL_1}.
+     */
+    @Deprecated
+    @SplitAttributes.ExtLayoutDirection
     public int getLayoutDirection() {
-        return mLayoutDirection;
+        return mDefaultSplitAttributes.getLayoutDirection();
+    }
+
+    /**
+     * Returns the default {@link SplitAttributes} which is applied if
+     * {@link #checkParentMetrics(WindowMetrics)} is {@code true}.
+     *
+     * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    @NonNull
+    public SplitAttributes getDefaultSplitAttributes() {
+        return mDefaultSplitAttributes;
     }
 
     @Override
@@ -118,16 +142,16 @@
         if (this == o) return true;
         if (!(o instanceof SplitRule)) return false;
         SplitRule that = (SplitRule) o;
-        return Float.compare(that.mSplitRatio, mSplitRatio) == 0
-                && mParentWindowMetricsPredicate.equals(that.mParentWindowMetricsPredicate)
-                && mLayoutDirection == that.mLayoutDirection;
+        return super.equals(that)
+                && mDefaultSplitAttributes.equals(that.mDefaultSplitAttributes)
+                && mParentWindowMetricsPredicate.equals(that.mParentWindowMetricsPredicate);
     }
 
     @Override
     public int hashCode() {
-        int result = (int) (mSplitRatio * 17);
+        int result = super.hashCode();
         result = 31 * result + mParentWindowMetricsPredicate.hashCode();
-        result = 31 * result + mLayoutDirection;
+        result = 31 * result + Objects.hashCode(mDefaultSplitAttributes);
         return result;
     }
 
@@ -135,8 +159,8 @@
     @Override
     public String toString() {
         return "SplitRule{"
-                + "mSplitRatio=" + mSplitRatio
-                + ", mLayoutDirection=" + mLayoutDirection
+                + "mTag=" + getTag()
+                + ", mDefaultSplitAttributes=" + mDefaultSplitAttributes
                 + '}';
     }
 }
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 3b6dd1a..1562d86 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
@@ -17,8 +17,10 @@
 package androidx.window.extensions.layout;
 
 import android.app.Activity;
+import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.UiContext;
 import androidx.window.extensions.WindowExtensions;
 
 import java.util.function.Consumer;
@@ -40,7 +42,13 @@
 public interface WindowLayoutComponent {
 
     /**
-     * Adds a listener interested in receiving updates to {@link WindowLayoutInfo}
+     * Adds a listener interested in receiving updates to {@link WindowLayoutInfo}.
+     * Use {@link WindowLayoutComponent#removeWindowLayoutInfoListener} to remove listener.
+     *
+     * A {@link Activity} or a Consumer instance can only be registered once.
+     * Registering the same {@link Activity} or Consumer more than once will result in
+     * a noop.
+     *
      * @param activity hosting a {@link android.view.Window}
      * @param consumer interested in receiving updates to {@link WindowLayoutInfo}
      */
@@ -48,6 +56,30 @@
             @NonNull Consumer<WindowLayoutInfo> consumer);
 
     /**
+     * Adds a listener interested in receiving updates to {@link WindowLayoutInfo}.
+     * Use {@link WindowLayoutComponent#removeWindowLayoutInfoListener} to remove listener.
+     *
+     * A {@link Context} or a Consumer instance can only be registered once.
+     * Registering the same {@link Context} or Consumer more than once will result in
+     * a noop.
+     *
+     * @param context a {@link UiContext} that corresponds to a window or an area on the
+     *                      screen - an {@link Activity}, a {@link Context} created with
+     *                      {@link Context#createWindowContext(Display, int , Bundle)}, or
+     *                      {@link android.inputmethodservice.InputMethodService}.
+     * @param consumer interested in receiving updates to {@link WindowLayoutInfo}
+     * @since {@link WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    // TODO(b/238905747): Add api guard for extensions.
+    @SuppressWarnings("PairedRegistration")
+    // The paired method for unregistering is also removeWindowLayoutInfoListener.
+    default void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
+            @NonNull Consumer<WindowLayoutInfo> consumer) {
+        throw new UnsupportedOperationException("This method must not be called unless there is a"
+                + " corresponding override implementation on the device.");
+    }
+
+    /**
      * Removes a listener no longer interested in receiving updates.
      * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
      */
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
new file mode 100644
index 0000000..76a1107
--- /dev/null
+++ b/window/extensions/extensions/src/test/java/androidx/window/extensions/embedding/SplitAttributesTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.window.extensions.embedding;
+
+import static androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType.splitEqually;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Color;
+
+import androidx.test.filters.SmallTest;
+import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection;
+
+import org.junit.Test;
+
+/** Test for {@link SplitAttributes} */
+@SmallTest
+public class SplitAttributesTest {
+    @Test
+    public void testSplitAttributesEquals() {
+        final SplitAttributes layout1 = new SplitAttributes.Builder()
+                .setSplitType(splitEqually())
+                .setLayoutDirection(LayoutDirection.LOCALE)
+                .setAnimationBackgroundColor(0)
+                .build();
+        final SplitAttributes layout2 = new SplitAttributes.Builder()
+                .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
+                .setLayoutDirection(LayoutDirection.LOCALE)
+                .setAnimationBackgroundColor(0)
+                .build();
+        final SplitAttributes layout3 = new SplitAttributes.Builder()
+                .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
+                .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
+                .setAnimationBackgroundColor(0)
+                .build();
+        final SplitAttributes layout4 = new SplitAttributes.Builder()
+                .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
+                .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
+                .setAnimationBackgroundColor(Color.BLUE)
+                .build();
+        final SplitAttributes layout5 = new SplitAttributes.Builder()
+                .setSplitType(new SplitAttributes.SplitType.HingeSplitType(splitEqually()))
+                .setLayoutDirection(LayoutDirection.TOP_TO_BOTTOM)
+                .setAnimationBackgroundColor(Color.BLUE)
+                .build();
+
+        assertNotEquals(layout1, layout2);
+        assertNotEquals(layout1.hashCode(), layout2.hashCode());
+
+        assertNotEquals(layout2, layout3);
+        assertNotEquals(layout2.hashCode(), layout3.hashCode());
+
+        assertNotEquals(layout3, layout1);
+        assertNotEquals(layout3.hashCode(), layout1.hashCode());
+
+        assertNotEquals(layout4, layout3);
+        assertNotEquals(layout4.hashCode(), layout3.hashCode());
+
+        assertEquals(layout4, layout5);
+        assertEquals(layout4.hashCode(), layout5.hashCode());
+    }
+
+    @Test
+    public void testSplitTypeEquals() {
+        final SplitAttributes.SplitType[] splitTypes = new SplitAttributes.SplitType[]{
+                new SplitAttributes.SplitType.ExpandContainersSplitType(),
+                new SplitAttributes.SplitType.RatioSplitType(0.3f),
+                splitEqually(),
+                new SplitAttributes.SplitType.HingeSplitType(splitEqually()),
+                new SplitAttributes.SplitType.HingeSplitType(
+                        new SplitAttributes.SplitType.ExpandContainersSplitType()
+                ),
+        };
+
+        for (int i = 0; i < splitTypes.length; i++) {
+            for (int j = 0; j < splitTypes.length; j++) {
+                final SplitAttributes.SplitType splitType0 = splitTypes[i];
+                final SplitAttributes.SplitType splitType1 = splitTypes[j];
+                if (i == j) {
+                    assertEquals(splitType0, splitType1);
+                    assertEquals(splitType0.hashCode(), splitType1.hashCode());
+                } else {
+                    assertNotEquals(splitType0, splitType1);
+                    assertNotEquals(splitType0.hashCode(), splitType1.hashCode());
+                }
+            }
+        }
+    }
+}
diff --git a/window/window-samples/build.gradle b/window/window-demos/demo-common/build.gradle
similarity index 61%
copy from window/window-samples/build.gradle
copy to window/window-demos/demo-common/build.gradle
index 99cf856..fd30ac1 100644
--- a/window/window-samples/build.gradle
+++ b/window/window-demos/demo-common/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * 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.
@@ -14,28 +14,24 @@
  * limitations under the License.
  */
 
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
 plugins {
     id("AndroidXPlugin")
-    id("com.android.application")
-    id("org.jetbrains.kotlin.android")
+    id("com.android.library")
+    id("kotlin-android")
 }
 
 android {
     defaultConfig {
-        applicationId "androidx.window.sample"
         minSdkVersion 23
     }
     buildFeatures {
         viewBinding true
     }
-    namespace "androidx.window.sample"
+    namespace "androidx.window.demo.common"
 }
 
 dependencies {
-    implementation("androidx.appcompat:appcompat:1.5.1")
+    implementation("androidx.appcompat:appcompat:1.2.0")
     implementation("androidx.core:core-ktx:1.3.2")
     implementation("androidx.activity:activity:1.2.0")
     implementation "androidx.recyclerview:recyclerview:1.2.1"
@@ -48,18 +44,4 @@
 
     implementation(project(":window:window-java"))
     debugImplementation(libs.leakcanary)
-
-    androidTestImplementation(libs.testCore)
-    androidTestImplementation(libs.testExtJunit)
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.testRules)
-    androidTestImplementation(libs.espressoCore, excludes.espresso)
-    androidTestImplementation(project(":window:window-testing"))
-}
-
-androidx {
-    name = "Jetpack WindowManager library samples"
-    publish = Publish.NONE
-    inceptionYear = "2020"
-    description = "Demo of Jetpack WindowManager library APIs"
 }
diff --git a/window/window-demos/demo-common/src/main/AndroidManifest.xml b/window/window-demos/demo-common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..134b96c
--- /dev/null
+++ b/window/window-demos/demo-common/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".DisplayFeaturesActivity"
+            android:exported="true"
+            android:supportsPictureInPicture="true"
+            android:configChanges=
+                "orientation|screenSize|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+            android:allowUntrustedActivityEmbedding="true"
+            android:label="@string/display_features_config_change" />
+    </application>
+</manifest>
diff --git a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesNoConfigChangeActivity.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
similarity index 85%
rename from window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesNoConfigChangeActivity.kt
rename to window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
index 729ceb3..1126232 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesNoConfigChangeActivity.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/DisplayFeaturesActivity.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo.common
 
 import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
@@ -25,31 +25,33 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.RecyclerView
+import androidx.window.demo.common.databinding.ActivityDisplayFeaturesConfigChangeBinding
+import androidx.window.demo.common.infolog.InfoLogAdapter
+import androidx.window.demo.common.util.PictureInPictureUtil.appendPictureInPictureMenu
+import androidx.window.demo.common.util.PictureInPictureUtil.handlePictureInPictureMenuItem
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowInfoTracker
 import androidx.window.layout.WindowLayoutInfo
-import androidx.window.sample.databinding.ActivityDisplayFeaturesNoConfigChangeBinding
-import androidx.window.sample.infolog.InfoLogAdapter
-import androidx.window.sample.util.PictureInPictureUtil.appendPictureInPictureMenu
-import androidx.window.sample.util.PictureInPictureUtil.handlePictureInPictureMenuItem
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
-class DisplayFeaturesNoConfigChangeActivity : AppCompatActivity() {
+/** Demo activity that shows all display features and current device state on the screen. */
+open class DisplayFeaturesActivity : AppCompatActivity() {
 
     private val infoLogAdapter = InfoLogAdapter()
     private val displayFeatureViews = ArrayList<View>()
-    private lateinit var binding: ActivityDisplayFeaturesNoConfigChangeBinding
+    private lateinit var binding: ActivityDisplayFeaturesConfigChangeBinding
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        binding = ActivityDisplayFeaturesNoConfigChangeBinding.inflate(layoutInflater)
+        binding = ActivityDisplayFeaturesConfigChangeBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
-        val recyclerView = binding.infoLogRecyclerView
+        val recyclerView = findViewById<RecyclerView>(R.id.infoLogRecyclerView)
         recyclerView.adapter = infoLogAdapter
 
         lifecycleScope.launch(Dispatchers.Main) {
@@ -59,8 +61,8 @@
             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 // Safely collect from windowInfoRepo when the lifecycle is STARTED
                 // and stops collection when the lifecycle is STOPPED
-                WindowInfoTracker.getOrCreate(this@DisplayFeaturesNoConfigChangeActivity)
-                    .windowLayoutInfo(this@DisplayFeaturesNoConfigChangeActivity)
+                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
+                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                     .collect { newLayoutInfo ->
                         // New posture information
                         updateStateLog(newLayoutInfo)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SampleTools.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/SampleTools.kt
similarity index 95%
rename from window/window-samples/src/main/java/androidx/window/sample/SampleTools.kt
rename to window/window-demos/demo-common/src/main/java/androidx/window/demo/common/SampleTools.kt
index b7c3a4d..ff88b99 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SampleTools.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/SampleTools.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * 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.
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.window.sample
+package androidx.window.demo.common
+
 import android.graphics.Rect
 import android.view.View
 import android.widget.FrameLayout
diff --git a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLog.kt
similarity index 89%
rename from window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt
rename to window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLog.kt
index e69a77b..3ab4096 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLog.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.infolog
+package androidx.window.demo.common.infolog
 
 /**
  * A data class to hold a title and a detail or subtitle that can be shown using [InfoLogAdapter]
diff --git a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLogAdapter.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
similarity index 82%
rename from window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLogAdapter.kt
rename to window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
index 29fefd6..81df057 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLogAdapter.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogAdapter.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.infolog
+package androidx.window.demo.common.infolog
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.recyclerview.widget.RecyclerView
-import androidx.window.sample.R
+import androidx.window.demo.common.R
 
 class InfoLogAdapter : RecyclerView.Adapter<InfoLogVH>() {
 
@@ -34,14 +34,19 @@
 
     override fun onBindViewHolder(holder: InfoLogVH, position: Int) {
         val item = items[position]
-        holder.titleView.text = "ID: ${item.id} Title: ${item.title}"
-        holder.detailView.text = "Detail: ${item.detail}"
+        holder.titleView.text = "[ID${item.id}] ${item.title}"
+        holder.detailView.text = item.detail
     }
 
     override fun getItemCount(): Int {
         return items.size
     }
 
+    fun clear() {
+        items.clear()
+        id = 0
+    }
+
     fun append(title: String, message: String) {
         append(InfoLog(title, message, id))
         ++id
diff --git a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLogVH.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogVH.kt
similarity index 86%
rename from window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLogVH.kt
rename to window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogVH.kt
index 4760be2..1bf7dfe 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLogVH.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/infolog/InfoLogVH.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.infolog
+package androidx.window.demo.common.infolog
 
 import android.view.View
 import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
-import androidx.window.sample.R
+import androidx.window.demo.common.R
 
 class InfoLogVH(view: View) : RecyclerView.ViewHolder(view) {
     val titleView: TextView = view.findViewById(R.id.title_view)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/util/PictureInPictureUtil.kt b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/util/PictureInPictureUtil.kt
similarity index 96%
rename from window/window-samples/src/main/java/androidx/window/sample/util/PictureInPictureUtil.kt
rename to window/window-demos/demo-common/src/main/java/androidx/window/demo/common/util/PictureInPictureUtil.kt
index 4f50715..70eedab 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/util/PictureInPictureUtil.kt
+++ b/window/window-demos/demo-common/src/main/java/androidx/window/demo/common/util/PictureInPictureUtil.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.util
+package androidx.window.demo.common.util
 
 import android.app.Activity
 import android.app.PictureInPictureParams
@@ -24,7 +24,7 @@
 import android.view.MenuItem
 import android.widget.Toast
 import androidx.annotation.RequiresApi
-import androidx.window.sample.R
+import androidx.window.demo.common.R
 
 @RequiresApi(Build.VERSION_CODES.O)
 private object PictureInPictureLauncherO {
diff --git a/window/window-samples/src/main/res/layout/activity_display_features_no_config_change.xml b/window/window-demos/demo-common/src/main/res/layout/activity_display_features_config_change.xml
similarity index 92%
rename from window/window-samples/src/main/res/layout/activity_display_features_no_config_change.xml
rename to window/window-demos/demo-common/src/main/res/layout/activity_display_features_config_change.xml
index 46ade36..419ab11 100644
--- a/window/window-samples/src/main/res/layout/activity_display_features_no_config_change.xml
+++ b/window/window-demos/demo-common/src/main/res/layout/activity_display_features_config_change.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2021 The Android Open Source Project
+  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.
@@ -22,7 +22,7 @@
     android:id="@+id/rootLayout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="androidx.window.sample.DisplayFeaturesNoConfigChangeActivity">
+    tools:context="androidx.window.demo.common.DisplayFeaturesActivity">
 
     <FrameLayout
         android:id="@+id/feature_container_layout"
@@ -38,7 +38,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:text="@string/current_state"
-        android:textAppearance="@style/TextAppearance.AppCompat.Large"
+        android:textAppearance="@style/TextAppearance.AppCompat"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.0"
         app:layout_constraintStart_toStartOf="parent"
@@ -87,7 +87,7 @@
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/current_state"
+        app:layout_constraintTop_toBottomOf="@id/current_state"
         app:layout_constraintBottom_toBottomOf="parent"/>
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/view_holder_info_log.xml b/window/window-demos/demo-common/src/main/res/layout/view_holder_info_log.xml
similarity index 93%
rename from window/window-samples/src/main/res/layout/view_holder_info_log.xml
rename to window/window-demos/demo-common/src/main/res/layout/view_holder_info_log.xml
index 7487737..e7e20c1 100644
--- a/window/window-samples/src/main/res/layout/view_holder_info_log.xml
+++ b/window/window-demos/demo-common/src/main/res/layout/view_holder_info_log.xml
@@ -24,7 +24,8 @@
     <TextView
         android:id="@+id/title_view"
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
+        android:layout_height="wrap_content"
+        android:textStyle="bold"/>
 
     <TextView
         android:id="@+id/detail_view"
diff --git a/window/window-samples/src/main/res/menu/picture_in_picture_menu.xml b/window/window-demos/demo-common/src/main/res/menu/picture_in_picture_menu.xml
similarity index 100%
copy from window/window-samples/src/main/res/menu/picture_in_picture_menu.xml
copy to window/window-demos/demo-common/src/main/res/menu/picture_in_picture_menu.xml
diff --git a/window/window-samples/src/main/res/values/colors.xml b/window/window-demos/demo-common/src/main/res/values/colors.xml
similarity index 85%
copy from window/window-samples/src/main/res/values/colors.xml
copy to window/window-demos/demo-common/src/main/res/values/colors.xml
index 41a72b2..be90b3a 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/window/window-demos/demo-common/src/main/res/values/colors.xml
@@ -16,9 +16,9 @@
   -->
 
 <resources>
-    <color name="colorPrimary">#6200EE</color>
-    <color name="colorPrimaryDark">#3700B3</color>
-    <color name="colorAccent">#03DAC5</color>
+    <color name="colorPrimary">#03A9F4</color>
+    <color name="colorPrimaryDark">#354395</color>
+    <color name="colorAccent">#009688</color>
 
     <color name="colorFeatureFold">#7700FF00</color>
 
diff --git a/window/window-demos/demo-common/src/main/res/values/strings.xml b/window/window-demos/demo-common/src/main/res/values/strings.xml
new file mode 100644
index 0000000..aeb9f83
--- /dev/null
+++ b/window/window-demos/demo-common/src/main/res/values/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <string name="trusted_embedding_activity">Trusted Embedding Activity</string>
+    <string name="trusted_embedding_activity_detail">Activity allows embedding in trusted mode via a
+        known certificate.</string>
+    <string name="untrusted_embedding_activity">Untrusted Embedding Activity</string>
+    <string name="untrusted_embedding_activity_detail">Activity allows embedding in untrusted mode
+        via opt-in.</string>
+    <string name="display_features_config_change">Display features handle config change</string>
+    <string name="fold">Fold</string>
+    <string name="legend">Legend:</string>
+    <string name="current_state">Current state</string>
+    <string name="window_layout">Window layout</string>
+    <string name="screens_are_separated">"Screens are separated"</string>
+    <string name="screens_are_not_separated">"Screen is not separated"</string>
+    <string name="screen_is_horizontal">"Hinge is horizontal"</string>
+    <string name="screen_is_vertical">"Hinge is vertical"</string>
+    <string name="occlusion_is_full">Full occlusion</string>
+    <string name="occlusion_is_none">No occlusion</string>
+</resources>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/values/styles.xml b/window/window-demos/demo-common/src/main/res/values/styles.xml
similarity index 94%
rename from window/window-samples/src/main/res/values/styles.xml
rename to window/window-demos/demo-common/src/main/res/values/styles.xml
index eaa9ab2..6f9733b 100644
--- a/window/window-samples/src/main/res/values/styles.xml
+++ b/window/window-demos/demo-common/src/main/res/values/styles.xml
@@ -1,5 +1,5 @@
 <!--
-  Copyright 2020 The Android Open Source Project
+  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.
diff --git a/window/window-demos/demo-second-app/build.gradle b/window/window-demos/demo-second-app/build.gradle
new file mode 100644
index 0000000..6af138b
--- /dev/null
+++ b/window/window-demos/demo-second-app/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.window.demo2"
+        minSdkVersion 23
+    }
+    buildFeatures {
+        viewBinding true
+    }
+    namespace "androidx.window.demo2"
+}
+
+dependencies {
+    implementation("androidx.activity:activity:1.2.0")
+    implementation("androidx.appcompat:appcompat:1.2.0")
+    api(libs.constraintLayout)
+    implementation("androidx.core:core-ktx:1.8.0")
+    // TODO(b/152245564) Conflicting dependencies cause IDE errors.
+    implementation("androidx.lifecycle:lifecycle-viewmodel:2.4.0-alpha02")
+    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha02")
+    implementation("androidx.recyclerview:recyclerview:1.2.1")
+    implementation(project(":window:window-java"))
+    implementation(project(":window:window-demos:demo-common"))
+}
diff --git a/window/window-demos/demo-second-app/src/main/AndroidManifest.xml b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4d9f6c1
--- /dev/null
+++ b/window/window-demos/demo-second-app/src/main/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application
+        android:label="Activity Embedding Sample"
+        android:supportsRtl="true">
+        <activity
+            android:name=".embedding.TrustedEmbeddingActivity"
+            android:exported="true"
+            android:label="@string/trusted_embedding_activity"
+            android:configChanges=
+                "orientation|screenSize|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+            android:knownActivityEmbeddingCerts=
+                "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".embedding.UntrustedEmbeddingActivity"
+            android:exported="true"
+            android:label="@string/untrusted_embedding_activity"
+            android:configChanges=
+                "orientation|screenSize|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+            android:allowUntrustedActivityEmbedding="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity-alias
+            android:name="androidx.window.demo2.DisplayFeaturesActivity"
+            android:targetActivity="androidx.window.demo.common.DisplayFeaturesActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
+    </application>
+</manifest>
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
new file mode 100644
index 0000000..bc2e7cd
--- /dev/null
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/TrustedEmbeddingActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.window.demo2.embedding
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.window.demo2.R
+
+/**
+ * Activity that can be embedded by a process with a known certificate. See
+ * `android:allowUntrustedActivityEmbedding` in AndroidManifest. Activity can be launched from the
+ * split demos in window-samples/demos.
+ */
+class TrustedEmbeddingActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_embedded)
+        findViewById<TextView>(R.id.detail_text_view).text =
+            getString(R.string.trusted_embedding_activity_detail)
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..f26aeee
--- /dev/null
+++ b/window/window-demos/demo-second-app/src/main/java/androidx/window/demo2/embedding/UntrustedEmbeddingActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.window.demo2.embedding
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.window.demo2.R
+
+/**
+ * Activity that can be embedded in untrusted mode. See
+ * `android:allowUntrustedActivityEmbedding` in AndroidManifest. Activity can be launched from
+ * the split demos in window-samples/demos.
+ */
+class UntrustedEmbeddingActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_embedded)
+        findViewById<TextView>(R.id.detail_text_view).text =
+            getString(R.string.untrusted_embedding_activity_detail)
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/test_ime.xml b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
similarity index 61%
rename from window/window-samples/src/main/res/layout/test_ime.xml
rename to window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
index feda1d7..fbd572c 100644
--- a/window/window-samples/src/main/res/layout/test_ime.xml
+++ b/window/window-demos/demo-second-app/src/main/res/layout/activity_embedded.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="utf-8"?>
+<!--
   Copyright 2022 The Android Open Source Project
 
   Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,15 +15,14 @@
   limitations under the License.
   -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="match_parent">
 
-    <Button
-        android:id="@+id/button_close"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="@string/test_ime_button_close"/>
+    <TextView
+        android:layout_gravity="center"
+        android:id="@+id/detail_text_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
 
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
\ 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
new file mode 100644
index 0000000..de4172b
--- /dev/null
+++ b/window/window-demos/demo-second-app/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <string name="trusted_embedding_activity">Trusted Embedding Activity</string>
+    <string name="trusted_embedding_activity_detail">Activity allows embedding in trusted mode via a
+        known certificate.</string>
+    <string name="untrusted_embedding_activity">Untrusted Embedding Activity</string>
+    <string name="untrusted_embedding_activity_detail">Activity allows embedding in untrusted mode
+        via opt-in.</string>
+</resources>
\ No newline at end of file
diff --git a/window/window-demos/demo/README.md b/window/window-demos/demo/README.md
new file mode 100644
index 0000000..f5f7378
--- /dev/null
+++ b/window/window-demos/demo/README.md
@@ -0,0 +1,6 @@
+# WindowManager Jetpack Demos
+
+The `keystore.jks` was generated using sample keys and certificates from Android AOSP. It is used to
+sign the demo app with a known key and showcase the usage of ActivityEmbedding APIs across apps
+for known certificates. See `build.gradle` for signing and
+`window-samples/demo-second-app` for the usage of the known certificate digest.
diff --git a/window/window-samples/build.gradle b/window/window-demos/demo/build.gradle
similarity index 71%
rename from window/window-samples/build.gradle
rename to window/window-demos/demo/build.gradle
index 99cf856..57c30e5 100644
--- a/window/window-samples/build.gradle
+++ b/window/window-demos/demo/build.gradle
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -25,13 +24,29 @@
 
 android {
     defaultConfig {
-        applicationId "androidx.window.sample"
+        applicationId "androidx.window.demo"
         minSdkVersion 23
     }
     buildFeatures {
         viewBinding true
     }
-    namespace "androidx.window.sample"
+    namespace "androidx.window.demo"
+    signingConfigs {
+        config {
+            keyAlias 'alias'
+            keyPassword 'password'
+            storeFile file('keystore.jks')
+            storePassword 'password'
+        }
+    }
+    buildTypes {
+        release {
+            signingConfig signingConfigs.config
+        }
+        debug {
+            signingConfig signingConfigs.config
+        }
+    }
 }
 
 dependencies {
@@ -47,6 +62,7 @@
     implementation("androidx.startup:startup-runtime:1.1.0")
 
     implementation(project(":window:window-java"))
+    implementation(project(":window:window-demos:demo-common"))
     debugImplementation(libs.leakcanary)
 
     androidTestImplementation(libs.testCore)
@@ -55,11 +71,12 @@
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(project(":window:window-testing"))
+    androidTestImplementation(project(":window:window-demos:demo-common"))
 }
 
-androidx {
-    name = "Jetpack WindowManager library samples"
-    publish = Publish.NONE
-    inceptionYear = "2020"
-    description = "Demo of Jetpack WindowManager library APIs"
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
+    }
 }
diff --git a/window/window-demos/demo/keystore.jks b/window/window-demos/demo/keystore.jks
new file mode 100644
index 0000000..35e18c9
--- /dev/null
+++ b/window/window-demos/demo/keystore.jks
Binary files differ
diff --git a/window/window-samples/src/androidTest/java/androidx/window/sample/DisplayFeaturesNoConfigChangeActivityTest.kt b/window/window-demos/demo/src/androidTest/java/androidx/window/demo/DisplayFeaturesNoConfigChangeActivityTest.kt
similarity index 98%
rename from window/window-samples/src/androidTest/java/androidx/window/sample/DisplayFeaturesNoConfigChangeActivityTest.kt
rename to window/window-demos/demo/src/androidTest/java/androidx/window/demo/DisplayFeaturesNoConfigChangeActivityTest.kt
index d0ff11b..53f1c78 100644
--- a/window/window-samples/src/androidTest/java/androidx/window/sample/DisplayFeaturesNoConfigChangeActivityTest.kt
+++ b/window/window-demos/demo/src/androidTest/java/androidx/window/demo/DisplayFeaturesNoConfigChangeActivityTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.assertion.ViewAssertions.matches
diff --git a/window/window-samples/src/androidTest/java/androidx/window/sample/SplitLayoutActivityTest.kt b/window/window-demos/demo/src/androidTest/java/androidx/window/demo/SplitLayoutActivityTest.kt
similarity index 98%
rename from window/window-samples/src/androidTest/java/androidx/window/sample/SplitLayoutActivityTest.kt
rename to window/window-demos/demo/src/androidTest/java/androidx/window/demo/SplitLayoutActivityTest.kt
index f15e2ed..d3aa678d 100644
--- a/window/window-samples/src/androidTest/java/androidx/window/sample/SplitLayoutActivityTest.kt
+++ b/window/window-demos/demo/src/androidTest/java/androidx/window/demo/SplitLayoutActivityTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import android.graphics.Rect
 import android.view.View
@@ -32,6 +32,7 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.window.demo.common.adjustFeaturePositionOffset
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.FoldingFeature.Orientation.Companion.HORIZONTAL
 import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-demos/demo/src/main/AndroidManifest.xml
similarity index 73%
rename from window/window-samples/src/main/AndroidManifest.xml
rename to window/window-demos/demo/src/main/AndroidManifest.xml
index 17431f8..1b8c38c 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-demos/demo/src/main/AndroidManifest.xml
@@ -19,7 +19,7 @@
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
 
-        <service android:name="androidx.window.sample.TestIme"
+        <service android:name=".TestIme"
             android:label="@string/test_ime"
             android:permission="android.permission.BIND_INPUT_METHOD"
             android:exported="true">
@@ -42,13 +42,6 @@
             android:exported="false"
             android:label="@string/presentation" />
         <activity
-            android:name=".DisplayFeaturesConfigChangeActivity"
-            android:exported="false"
-            android:supportsPictureInPicture="true"
-            android:configChanges=
-                "orientation|screenSize|screenLayout|screenSize|layoutDirection|smallestScreenSize"
-            android:label="@string/display_features_config_change" />
-        <activity
             android:name=".DisplayFeaturesNoConfigChangeActivity"
             android:exported="false"
             android:supportsPictureInPicture="true"
@@ -61,12 +54,22 @@
             android:exported="false"
             android:configChanges="orientation|screenSize|screenLayout|screenSize"
             android:label="@string/window_metrics"/>
+        <activity android:name=".RearDisplayActivityConfigChanges"
+            android:exported="true"
+            android:configChanges=
+                "orientation|screenLayout|screenSize|layoutDirection|smallestScreenSize"
+            android:label="@string/rear_display">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
         <activity
             android:name=".embedding.SplitActivityA"
             android:exported="true"
             android:label="Split Main"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity">
+            android:taskAffinity="androidx.window.demo.manual_split_affinity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -77,43 +80,50 @@
             android:exported="false"
             android:label="B"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityC"
             android:exported="false"
             android:label="C"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityD"
             android:exported="false"
             android:label="D"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityE"
             android:exported="false"
             android:label="E"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityF"
             android:exported="false"
             android:label="F"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityPlaceholder"
             android:exported="false"
             android:label="Placeholder"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.manual_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
+        <activity
+            android:name=".embedding.ExpandedDialogActivity"
+            android:theme="@style/ExpandedDialogTheme"
+            android:exported="false"
+            android:label="Dialog Activity"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.manual_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityTrampoline"
             android:exported="true"
             android:label="Split Trampoline"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.trampoline_affinity">
+            android:taskAffinity="androidx.window.demo.trampoline_affinity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -127,7 +137,7 @@
             android:exported="true"
             android:label="Split List"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.list_detail_split_affinity">
+            android:taskAffinity="androidx.window.demo.list_detail_split_affinity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -139,13 +149,13 @@
             android:label="Item detail"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:launchMode="singleTop"
-            android:taskAffinity="androidx.window.sample.list_detail_split_affinity"/>
+            android:taskAffinity="androidx.window.demo.list_detail_split_affinity"/>
         <activity
             android:name=".embedding.SplitActivityListPlaceholder"
             android:exported="false"
             android:label="Placeholder"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.list_detail_split_affinity" />
+            android:taskAffinity="androidx.window.demo.list_detail_split_affinity" />
 
         <!-- Split PiP App -->
 
@@ -155,7 +165,7 @@
             android:label="Split and PiP"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:supportsPictureInPicture="true"
-            android:taskAffinity="androidx.window.sample.split_pip">
+            android:taskAffinity="androidx.window.demo.split_pip">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -167,21 +177,43 @@
             android:label="PiP B"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
             android:supportsPictureInPicture="true"
-            android:taskAffinity="androidx.window.sample.split_pip">
+            android:taskAffinity="androidx.window.demo.split_pip">
         </activity>
         <activity
             android:name=".embedding.SplitPipActivityNoPip"
             android:exported="false"
             android:label="No PiP support"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.split_pip">
+            android:taskAffinity="androidx.window.demo.split_pip">
         </activity>
         <activity
             android:name=".embedding.SplitPipActivityPlaceholder"
             android:exported="false"
             android:label="PiP Placeholder"
             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
-            android:taskAffinity="androidx.window.sample.split_pip">
+            android:taskAffinity="androidx.window.demo.split_pip">
+        </activity>
+
+        <!-- The demo App to show how to change the current split layout with the current device and
+         window states -->
+
+        <activity
+            android:name=".embedding.SplitDeviceStateActivityA"
+            android:exported="true"
+            android:label="Split on Device State"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.split_device_state_activity_affinity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".embedding.SplitDeviceStateActivityB"
+            android:exported="true"
+            android:label="Split on Device State B"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density|touchscreen"
+            android:taskAffinity="androidx.window.demo.split_device_state_activity_affinity">
         </activity>
 
         <!-- The demo app that shows various IME-related use cases -->
@@ -198,7 +230,7 @@
             android:exported="false"
             tools:node="merge">
             <!-- This entry makes ExampleWindowInitializer discoverable. -->
-            <meta-data  android:name="androidx.window.sample.embedding.ExampleWindowInitializer"
+            <meta-data  android:name="androidx.window.demo.embedding.ExampleWindowInitializer"
                 android:value="androidx.startup" />
         </provider>
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/DisplayFeaturesNoConfigChangeActivity.kt
similarity index 78%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt
copy to window/window-demos/demo/src/main/java/androidx/window/demo/DisplayFeaturesNoConfigChangeActivity.kt
index 754e11d8..bd77218 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/DisplayFeaturesNoConfigChangeActivity.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo
 
-open class SplitActivityListPlaceholder : SplitActivityPlaceholder()
\ No newline at end of file
+import androidx.window.demo.common.DisplayFeaturesActivity
+
+class DisplayFeaturesNoConfigChangeActivity : DisplayFeaturesActivity()
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
similarity index 97%
rename from window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
index 5ee6536..a31144d 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/ImeActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import android.content.Intent
 import android.os.Bundle
diff --git a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
similarity index 99%
rename from window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
index d9001c8..5b4cb9c 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/PresentationActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import android.app.Presentation
 import android.content.Context
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/RearDisplayActivityConfigChanges.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/RearDisplayActivityConfigChanges.kt
new file mode 100644
index 0000000..41c62be
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/RearDisplayActivityConfigChanges.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 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.window.demo
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaController
+import androidx.window.area.WindowAreaSessionCallback
+import androidx.window.area.WindowAreaSession
+import androidx.window.area.WindowAreaStatus
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.java.area.WindowAreaControllerJavaAdapter
+import androidx.window.demo.databinding.ActivityRearDisplayBinding
+import androidx.window.demo.common.infolog.InfoLogAdapter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.Executor
+
+/**
+ * Demo Activity that showcases listening for RearDisplay Status
+ * as well as enabling/disabling RearDisplay mode. This Activity
+ * implements [WindowAreaSessionCallback] for simplicity.
+ *
+ * This Activity overrides configuration changes for simplicity.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class RearDisplayActivityConfigChanges : AppCompatActivity(), WindowAreaSessionCallback {
+
+    private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
+    private var rearDisplaySession: WindowAreaSession? = null
+    private val infoLogAdapter = InfoLogAdapter()
+    private lateinit var binding: ActivityRearDisplayBinding
+    private lateinit var executor: Executor
+
+    private val rearDisplayStatusListener = Consumer<WindowAreaStatus> { status ->
+        infoLogAdapter.append(getCurrentTimeString(), status.toString())
+        infoLogAdapter.notifyDataSetChanged()
+        updateRearDisplayButton(status)
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = ActivityRearDisplayBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        executor = ContextCompat.getMainExecutor(this)
+        windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())
+
+        binding.rearStatusRecyclerView.adapter = infoLogAdapter
+
+        binding.rearDisplayButton.setOnClickListener {
+            if (rearDisplaySession != null) {
+                rearDisplaySession?.close()
+            } else {
+                windowAreaController.startRearDisplayModeSession(
+                    this,
+                    executor,
+                    this)
+            }
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        windowAreaController.addRearDisplayStatusListener(
+            executor,
+            rearDisplayStatusListener
+        )
+    }
+
+    override fun onStop() {
+        super.onStop()
+        windowAreaController.removeRearDisplayStatusListener(rearDisplayStatusListener)
+    }
+
+    override fun onSessionStarted(session: WindowAreaSession) {
+        rearDisplaySession = session
+        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has been started")
+        infoLogAdapter.notifyDataSetChanged()
+    }
+
+    override fun onSessionEnded() {
+        rearDisplaySession = null
+        infoLogAdapter.append(getCurrentTimeString(), "RearDisplay Session has ended")
+        infoLogAdapter.notifyDataSetChanged()
+    }
+
+    private fun updateRearDisplayButton(status: WindowAreaStatus) {
+        if (rearDisplaySession != null) {
+            binding.rearDisplayButton.isEnabled = true
+            binding.rearDisplayButton.text = "Disable RearDisplay Mode"
+            return
+        }
+        when (status) {
+            WindowAreaStatus.UNSUPPORTED -> {
+                binding.rearDisplayButton.isEnabled = false
+                binding.rearDisplayButton.text = "RearDisplay is not supported on this device"
+            }
+            WindowAreaStatus.UNAVAILABLE -> {
+                binding.rearDisplayButton.isEnabled = false
+                binding.rearDisplayButton.text = "RearDisplay is not currently available"
+            }
+            WindowAreaStatus.AVAILABLE -> {
+                binding.rearDisplayButton.isEnabled = true
+                binding.rearDisplayButton.text = "Enable RearDisplay Mode"
+            }
+        }
+    }
+
+    private fun getCurrentTimeString(): String {
+        val sdf = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
+        val currentDate = sdf.format(Date())
+        return currentDate.toString()
+    }
+
+    private companion object {
+        private val TAG = RearDisplayActivityConfigChanges::class.java.simpleName
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayout.kt
similarity index 98%
rename from window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayout.kt
index ca17b23..06ba0be 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayout.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import android.content.Context
 import android.graphics.Rect
@@ -23,6 +23,7 @@
 import android.view.View.MeasureSpec.AT_MOST
 import android.view.View.MeasureSpec.EXACTLY
 import android.widget.FrameLayout
+import androidx.window.demo.common.adjustFeaturePositionOffset
 import androidx.window.layout.DisplayFeature
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.WindowLayoutInfo
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
similarity index 98%
rename from window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
index 0bcdc5d..89290a9e 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/SplitLayoutActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
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
new file mode 100644
index 0000000..a63b171
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/TestIme.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.window.demo
+
+import android.inputmethodservice.InputMethodService
+import android.os.Build
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+import androidx.core.view.WindowInsetsCompat.Type
+import androidx.recyclerview.widget.RecyclerView
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.demo.common.infolog.InfoLogAdapter
+import androidx.window.layout.WindowMetrics
+import androidx.window.layout.WindowMetricsCalculator
+
+/**
+ * A test IME that currently provides a minimal UI containing a "Close" button. To use this, go to
+ * "Settings > System > Languages & Input > On-screen keyboard" and enable "Test IME". Remember you
+ * may still need to switch to this IME after the default on-screen keyboard pops up.
+ */
+internal class TestIme : InputMethodService() {
+
+    private val adapter = InfoLogAdapter()
+
+    override fun onCreateInputView(): View {
+        return layoutInflater.inflate(R.layout.test_ime, null).apply {
+            findViewById<RecyclerView>(R.id.recycler_view).adapter = adapter
+
+            findViewById<Button>(R.id.button_clear).setOnClickListener {
+                adapter.clear()
+                adapter.notifyDataSetChanged()
+            }
+
+            findViewById<Button>(R.id.button_close).setOnClickListener {
+                requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS)
+            }
+
+            displayCurrentWindowMetrics()
+            displayMaximumWindowMetrics()
+        }
+    }
+
+    private fun displayCurrentWindowMetrics() {
+        val windowMetrics = WindowMetricsCalculator.getOrCreate()
+            .computeCurrentWindowMetrics(this@TestIme)
+        displayWindowMetrics("CurrentWindowMetrics update", windowMetrics)
+    }
+
+    private fun displayMaximumWindowMetrics() {
+        val windowMetrics = WindowMetricsCalculator.getOrCreate()
+            .computeMaximumWindowMetrics(this@TestIme)
+        displayWindowMetrics("MaximumWindowMetrics update", windowMetrics)
+    }
+
+    @OptIn(ExperimentalWindowApi::class)
+    private fun displayWindowMetrics(title: String, windowMetrics: WindowMetrics) {
+
+        val width = windowMetrics.bounds.width()
+        val height = windowMetrics.bounds.height()
+
+        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}")
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            val windowInsets = windowMetrics.getWindowInsets()
+            logBuilder.append("\nimeInset: ${windowInsets.getInsets(Type.ime())}")
+            logBuilder.append("\nnavInset: ${windowInsets.getInsets(Type.navigationBars())}")
+            logBuilder.append("\nstatusBarInset: ${windowInsets.getInsets(Type.statusBars())}")
+        }
+        adapter.append(title, logBuilder.toString())
+        adapter.notifyDataSetChanged()
+    }
+
+    override fun onEvaluateFullscreenMode(): Boolean {
+        return false
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/WindowMetricsActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
similarity index 94%
rename from window/window-samples/src/main/java/androidx/window/sample/WindowMetricsActivity.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
index 525648b..4b0faa7 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/WindowMetricsActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/WindowMetricsActivity.kt
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.window.sample
+package androidx.window.demo
 
 import android.content.res.Configuration
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.RecyclerView
 import androidx.window.layout.WindowMetricsCalculator
-import androidx.window.sample.infolog.InfoLogAdapter
+import androidx.window.demo.common.infolog.InfoLogAdapter
 
 class WindowMetricsActivity : AppCompatActivity() {
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoAdapter.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoAdapter.kt
similarity index 94%
rename from window/window-samples/src/main/java/androidx/window/sample/demos/DemoAdapter.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoAdapter.kt
index 8cb8aca..5bc46ae 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoAdapter.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoAdapter.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.demos
+package androidx.window.demo.demos
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.recyclerview.widget.RecyclerView
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 class DemoAdapter(private val demoItems: List<DemoItem>) : RecyclerView.Adapter<DemoVH>() {
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoItem.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoItem.kt
similarity index 94%
rename from window/window-samples/src/main/java/androidx/window/sample/demos/DemoItem.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoItem.kt
index fb08562..88f3c65 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoItem.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoItem.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.window.sample.demos
+package androidx.window.demo.demos
 
 import android.app.Activity
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoVH.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoVH.kt
similarity index 94%
rename from window/window-samples/src/main/java/androidx/window/sample/demos/DemoVH.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoVH.kt
index 5c08cc1..d986288 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoVH.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/DemoVH.kt
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.demos
+package androidx.window.demo.demos
 
 import android.content.Intent
 import android.view.View
 import android.widget.Button
 import android.widget.TextView
 import androidx.recyclerview.widget.RecyclerView
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 class DemoVH(view: View) : RecyclerView.ViewHolder(view) {
     private val description = view.findViewById<TextView>(R.id.demo_description)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
similarity index 76%
rename from window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
index dd2f206..19c0156 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/demos/WindowDemosActivity.kt
@@ -14,22 +14,22 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.demos
+package androidx.window.demo.demos
 
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.RecyclerView
-import androidx.window.sample.DisplayFeaturesConfigChangeActivity
-import androidx.window.sample.DisplayFeaturesNoConfigChangeActivity
-import androidx.window.sample.ImeActivity
-import androidx.window.sample.PresentationActivity
-import androidx.window.sample.R
-import androidx.window.sample.R.string.display_features_config_change
-import androidx.window.sample.R.string.display_features_no_config_change
-import androidx.window.sample.R.string.show_all_display_features_config_change_description
-import androidx.window.sample.R.string.show_all_display_features_no_config_change_description
-import androidx.window.sample.SplitLayoutActivity
-import androidx.window.sample.WindowMetricsActivity
+import androidx.window.demo.DisplayFeaturesNoConfigChangeActivity
+import androidx.window.demo.ImeActivity
+import androidx.window.demo.PresentationActivity
+import androidx.window.demo.R
+import androidx.window.demo.R.string.display_features_config_change
+import androidx.window.demo.R.string.display_features_no_config_change
+import androidx.window.demo.R.string.show_all_display_features_config_change_description
+import androidx.window.demo.R.string.show_all_display_features_no_config_change_description
+import androidx.window.demo.SplitLayoutActivity
+import androidx.window.demo.WindowMetricsActivity
+import androidx.window.demo.common.DisplayFeaturesActivity
 
 /**
  * Main activity that launches WindowManager demos.
@@ -43,7 +43,7 @@
             DemoItem(
                 buttonTitle = getString(display_features_config_change),
                 description = getString(show_all_display_features_config_change_description),
-                clazz = DisplayFeaturesConfigChangeActivity::class.java
+                clazz = DisplayFeaturesActivity::class.java
             ),
             DemoItem(
                 buttonTitle = getString(display_features_no_config_change),
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
new file mode 100644
index 0000000..0d3771e
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/DemoActivityEmbeddingController.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.window.demo.embedding
+
+import androidx.annotation.ColorInt
+import androidx.annotation.GuardedBy
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/** A singleton controller to manage the global config. */
+class DemoActivityEmbeddingController private constructor() {
+
+    private val lock = Object()
+
+    @GuardedBy("lock")
+    @ColorInt
+    private var _animationBackgroundColor = 0
+
+    /** Animation background color to use when the animation requires a background. */
+    var animationBackgroundColor: Int
+        @ColorInt
+        get() = synchronized(lock) {
+            _animationBackgroundColor
+        }
+        set(@ColorInt value) = synchronized(lock) {
+            _animationBackgroundColor = value
+        }
+
+    companion object {
+        @Volatile
+        private var globalInstance: DemoActivityEmbeddingController? = null
+        private val globalLock = ReentrantLock()
+
+        /**
+         * Obtains the singleton instance of [DemoActivityEmbeddingController].
+         */
+        @JvmStatic
+        fun getInstance(): DemoActivityEmbeddingController {
+            if (globalInstance == null) {
+                globalLock.withLock {
+                    if (globalInstance == null) {
+                        globalInstance = DemoActivityEmbeddingController()
+                    }
+                }
+            }
+            return globalInstance!!
+        }
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..fde6241
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExampleWindowInitializer.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright 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.window.demo.embedding
+
+import android.content.Context
+import androidx.startup.Initializer
+import androidx.window.demo.R
+import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE
+import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP
+import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.SUFFIX_REVERSED
+import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE
+import androidx.window.demo.embedding.SplitDeviceStateActivityBase.Companion.TAG_SHOW_FULLSCREEN_IN_PORTRAIT
+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.RuleController
+import androidx.window.embedding.SplitAttributes
+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.RIGHT_TO_LEFT
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
+import androidx.window.embedding.SplitAttributesCalculator
+import androidx.window.embedding.SplitController
+import androidx.window.layout.FoldingFeature
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+
+/**
+ * Initializes SplitController with a set of statically defined rules.
+ */
+class ExampleWindowInitializer : Initializer<RuleController> {
+    override fun create(context: Context): RuleController {
+        SplitController.getInstance(context).apply {
+            if (isSplitAttributesCalculatorSupported()) {
+                setSplitAttributesCalculator(SampleSplitAttributesCalculator())
+            }
+        }
+        return RuleController.getInstance(context).apply {
+            setRules(RuleController.parseRules(context, R.xml.main_split_config))
+        }
+    }
+
+    /**
+     * A sample [SplitAttributesCalculator] to demonstrate how to change the [SplitAttributes] with
+     * the current device and window state and
+     * [SplitAttributesCalculator.SplitAttributesCalculatorParams.splitRuleTag].
+     */
+    private class SampleSplitAttributesCalculator : SplitAttributesCalculator {
+
+        private val mDemoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
+        override fun computeSplitAttributesForParams(
+            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
+        ): SplitAttributes {
+            val isPortrait = params.parentWindowMetrics.isPortrait()
+            val windowLayoutInfo = params.parentWindowLayoutInfo
+            val isTabletop = windowLayoutInfo.isTabletop()
+            val isBookMode = windowLayoutInfo.isBookMode()
+            val config = params.parentConfiguration
+            // The SplitAttributes to occupy the whole task bounds
+            val expandContainersAttrs = SplitAttributes.Builder()
+                .setSplitType(SplitAttributes.SplitType.expandContainers())
+                .build()
+            val tag = params.splitRuleTag
+            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.
+            val backgroundColor = mDemoActivityEmbeddingController.animationBackgroundColor
+            val defaultSplitAttributes = SplitAttributes.Builder()
+                .setLayoutDirection(params.defaultSplitAttributes.layoutDirection)
+                .setSplitType(params.defaultSplitAttributes.splitType)
+                .setAnimationBackgroundColor(backgroundColor)
+                .build()
+            when (tag?.substringBefore(SUFFIX_REVERSED)) {
+                TAG_USE_DEFAULT_SPLIT_ATTRIBUTES, null -> {
+                    return if (params.isDefaultMinSizeSatisfied) {
+                        defaultSplitAttributes
+                    } else {
+                        expandContainersAttrs
+                    }
+                }
+                TAG_SHOW_FULLSCREEN_IN_PORTRAIT -> {
+                    if (isPortrait) {
+                        return expandContainersAttrs
+                    }
+                }
+                TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
+                    if (isTabletop) {
+                        return SplitAttributes.Builder()
+                            .setSplitType(SplitAttributes.SplitType.splitByHinge())
+                            .setLayoutDirection(
+                                if (shouldReversed) {
+                                    BOTTOM_TO_TOP
+                                } else {
+                                    TOP_TO_BOTTOM
+                                }
+                            )
+                            .setAnimationBackgroundColor(backgroundColor)
+                            .build()
+                    } else if (isPortrait) {
+                        return expandContainersAttrs
+                    }
+                }
+                TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP -> {
+                    if (isTabletop) {
+                        return SplitAttributes.Builder()
+                            .setSplitType(SplitAttributes.SplitType.splitByHinge())
+                            .setLayoutDirection(
+                                if (shouldReversed) {
+                                    BOTTOM_TO_TOP
+                                } else {
+                                    TOP_TO_BOTTOM
+                                }
+                            )
+                            .setAnimationBackgroundColor(backgroundColor)
+                            .build()
+                    }
+                }
+                TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE -> {
+                    return SplitAttributes.Builder()
+                        .setSplitType(SplitAttributes.SplitType.splitEqually())
+                        .setLayoutDirection(
+                            if (config.screenWidthDp <= 600) {
+                                if (shouldReversed) BOTTOM_TO_TOP else TOP_TO_BOTTOM
+                            } else {
+                                if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
+                            }
+                        )
+                        .setAnimationBackgroundColor(backgroundColor)
+                        .build()
+                }
+                TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE -> {
+                    return if (isBookMode) {
+                        expandContainersAttrs
+                    } else if (config.screenWidthDp <= 600) {
+                        SplitAttributes.Builder()
+                            .setSplitType(SplitAttributes.SplitType.splitEqually())
+                            .setLayoutDirection(
+                                if (shouldReversed) {
+                                    BOTTOM_TO_TOP
+                                } else {
+                                    TOP_TO_BOTTOM
+                                }
+                            )
+                            .setAnimationBackgroundColor(backgroundColor)
+                            .build()
+                    } else {
+                        SplitAttributes.Builder()
+                            .setSplitType(SplitAttributes.SplitType.splitEqually())
+                            .setLayoutDirection(
+                                if (shouldReversed) {
+                                    RIGHT_TO_LEFT
+                                } else {
+                                    LEFT_TO_RIGHT
+                                }
+                            )
+                            .setAnimationBackgroundColor(backgroundColor)
+                            .build()
+                    }
+                }
+                TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING -> {
+                    val foldingState = windowLayoutInfo.getFoldingFeature()
+                    if (foldingState != null) {
+                        return SplitAttributes.Builder()
+                            .setSplitType(
+                                if (foldingState.isSeparating) {
+                                    SplitAttributes.SplitType.splitByHinge()
+                                } else {
+                                    SplitAttributes.SplitType.ratio(0.3f)
+                                }
+                            ).setLayoutDirection(
+                                if (
+                                    foldingState.orientation
+                                        == FoldingFeature.Orientation.HORIZONTAL
+                                ) {
+                                    if (shouldReversed) BOTTOM_TO_TOP else TOP_TO_BOTTOM
+                                } else {
+                                    if (shouldReversed) RIGHT_TO_LEFT else LEFT_TO_RIGHT
+                                }
+                            )
+                            .setAnimationBackgroundColor(backgroundColor)
+                            .build()
+                    }
+                }
+            }
+            return defaultSplitAttributes
+        }
+
+        private fun WindowMetrics.isPortrait(): Boolean =
+            bounds.height() > bounds.width()
+
+        private fun WindowLayoutInfo.isTabletop(): Boolean {
+            val foldingFeature = getFoldingFeature()
+            return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
+                foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
+        }
+
+        private fun WindowLayoutInfo.isBookMode(): Boolean {
+            val foldingFeature = getFoldingFeature()
+            return foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
+                foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL
+        }
+
+        /**
+         * Returns the [FoldingFeature] if it is exactly the only [FoldingFeature] in
+         * [WindowLayoutInfo]. Otherwise, returns `null`.
+         */
+        private fun WindowLayoutInfo.getFoldingFeature(): FoldingFeature? {
+            val foldingFeatures = displayFeatures.filterIsInstance<FoldingFeature>()
+            return if (foldingFeatures.size == 1) foldingFeatures[0] else null
+        }
+    }
+
+    override fun dependencies(): List<Class<out Initializer<*>>> {
+        return emptyList()
+    }
+}
\ No newline at end of file
diff --git a/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
new file mode 100644
index 0000000..3419fcb
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/ExpandedDialogActivity.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.window.demo.embedding
+
+import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+
+/** Activity to show a dialog. */
+class ExpandedDialogActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        showDialog()
+    }
+
+    private fun showDialog() {
+        val dialog = AlertDialog.Builder(this)
+            .setTitle("Dialog in expanded activity")
+            .setMessage("To demo showing dialog that can expand over a split")
+            .setNeutralButton("Close") { _, _ ->
+                finish()
+            }
+            .setOnDismissListener {
+                finish()
+            }
+
+        dialog.show()
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityA.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityA.kt
similarity index 93%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityA.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityA.kt
index 20a8056..94757d6 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityA.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityA.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 open class SplitActivityA : SplitActivityBase()
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
similarity index 94%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityB.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
index 6791b5c..90a67dd 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityB.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityB.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.content.Intent
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 open class SplitActivityB : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
similarity index 63%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
index c3acc7a..0f48b67 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityBase.java
@@ -14,35 +14,42 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding;
+package androidx.window.demo.embedding;
 
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 
+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.PendingIntent;
+import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
 import android.widget.CompoundButton;
+import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.util.Consumer;
+import androidx.window.demo.R;
+import androidx.window.demo.databinding.ActivitySplitActivityLayoutBinding;
 import androidx.window.embedding.ActivityEmbeddingController;
 import androidx.window.embedding.ActivityFilter;
 import androidx.window.embedding.ActivityRule;
 import androidx.window.embedding.EmbeddingRule;
 import androidx.window.embedding.RuleController;
+import androidx.window.embedding.SplitAttributes;
 import androidx.window.embedding.SplitController;
 import androidx.window.embedding.SplitInfo;
 import androidx.window.embedding.SplitPairFilter;
 import androidx.window.embedding.SplitPairRule;
 import androidx.window.embedding.SplitPlaceholderRule;
-import androidx.window.embedding.SplitRule;
-import androidx.window.sample.databinding.ActivitySplitActivityLayoutBinding;
 
 import java.util.HashSet;
 import java.util.List;
@@ -61,9 +68,7 @@
     static final String EXTRA_LAUNCH_C_TO_SIDE = "launch_c_to_side";
 
     private SplitController mSplitController;
-
-    private final RuleController mRuleController = RuleController.getInstance(this);
-
+    private RuleController mRuleController;
     private SplitInfoCallback mCallback;
 
     private ActivitySplitActivityLayoutBinding mViewBinding;
@@ -77,7 +82,7 @@
         mViewBinding = ActivitySplitActivityLayoutBinding.inflate(getLayoutInflater());
         setContentView(mViewBinding.getRoot());
 
-        // Setup activity launch buttons.
+        // Setup activity launch buttons and config options.
         mViewBinding.launchB.setOnClickListener((View v) ->
                 startActivity(new Intent(this, SplitActivityB.class)));
         mViewBinding.launchBAndC.setOnClickListener((View v) -> {
@@ -97,6 +102,50 @@
                 Log.e(TAG, e.getMessage());
             }
         });
+        mViewBinding.launchUid2Trusted.setOnClickListener((View v) -> {
+            final Intent intent = new Intent();
+            // Use an explicit package and class name to start an Activity from a different
+            // package/UID.
+            intent.setClassName(
+                    "androidx.window.demo2",
+                    "androidx.window.demo2.embedding.TrustedEmbeddingActivity"
+            );
+            try {
+                startActivity(intent);
+            } catch (ActivityNotFoundException e) {
+                Toast.makeText(this, R.string.install_samples_2, Toast.LENGTH_LONG).show();
+            }
+        });
+        mViewBinding.launchUid2Untrusted.setOnClickListener((View v) -> {
+            final Intent intent = new Intent();
+            // Use an explicit package and class name to start an Activity from a different
+            // package/UID.
+            intent.setClassName(
+                    "androidx.window.demo2",
+                    "androidx.window.demo2.embedding.UntrustedEmbeddingActivity"
+            );
+            try {
+                startActivity(intent);
+            } catch (ActivityNotFoundException e) {
+                Toast.makeText(this, R.string.install_samples_2, Toast.LENGTH_LONG).show();
+            }
+        });
+        mViewBinding.launchUid2UntrustedDisplayFeatures.setOnClickListener((View v) -> {
+            final Intent intent = new Intent();
+            // Use an explicit package and class name to start an Activity from a different
+            // package/UID.
+            intent.setClassName(
+                    "androidx.window.demo2",
+                    "androidx.window.demo.common.DisplayFeaturesActivity"
+            );
+            try {
+                startActivity(intent);
+            } catch (ActivityNotFoundException e) {
+                Toast.makeText(this, R.string.install_samples_2, Toast.LENGTH_LONG).show();
+            }
+        });
+        mViewBinding.launchExpandedDialogButton.setOnClickListener((View v) ->
+                startActivity(new Intent(this, ExpandedDialogActivity.class)));
 
         // Listen for split configuration checkboxes to update the rules before launching
         // activities.
@@ -109,6 +158,7 @@
         mViewBinding.splitWithFCheckBox.setOnCheckedChangeListener(this);
 
         mSplitController = SplitController.getInstance(this);
+        mRuleController = RuleController.getInstance(this);
     }
 
     @Override
@@ -177,8 +227,8 @@
         mViewBinding.splitBCCheckBox.setChecked(bAndCPairConfig != null);
         mViewBinding.finishBCCheckBox.setEnabled(bAndCPairConfig != null);
         mViewBinding.finishBCCheckBox.setChecked(bAndCPairConfig != null
-                && bAndCPairConfig.getFinishPrimaryWithSecondary() == SplitRule.FINISH_ALWAYS
-                && bAndCPairConfig.getFinishSecondaryWithPrimary() == SplitRule.FINISH_ALWAYS);
+                && bAndCPairConfig.getFinishPrimaryWithSecondary() == ALWAYS
+                && bAndCPairConfig.getFinishSecondaryWithPrimary() == ALWAYS);
 
         SplitPairRule fConfig = getRuleFor(null, SplitActivityF.class);
         mViewBinding.splitWithFCheckBox.setChecked(fConfig != null);
@@ -259,85 +309,102 @@
     /** Updates the split rules based on the current selection on checkboxes. */
     private void updateRulesFromCheckboxes() {
         mRuleController.clearRules();
-
-        Set<SplitPairFilter> pairFilters = new HashSet<>();
-        pairFilters.add(new SplitPairFilter(componentName(SplitActivityA.class),
-                componentName("*"), null));
-        SplitPairRule rule = new SplitPairRule.Builder(pairFilters)
-                .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
-                .setMinSmallestWidthDp(0)
-                .setFinishPrimaryWithSecondary(SplitRule.FINISH_NEVER)
-                .setFinishSecondaryWithPrimary(SplitRule.FINISH_NEVER)
-                .setClearTop(true)
-                .setSplitRatio(SPLIT_RATIO)
+        final SplitAttributes defaultSplitAttributes = new SplitAttributes.Builder()
+                .setSplitType(SplitAttributes.SplitType.ratio(SPLIT_RATIO))
                 .build();
+
         if (mViewBinding.splitMainCheckBox.isChecked()) {
+            // Split main with any activity.
+            final Set<SplitPairFilter> pairFilters = new HashSet<>();
+            pairFilters.add(new SplitPairFilter(componentName(SplitActivityA.class),
+                    new ComponentName("*", "*"), null));
+            final SplitPairRule rule = new SplitPairRule.Builder(pairFilters)
+                    .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
+                    .setMinHeightDp(0)
+                    .setMinSmallestWidthDp(0)
+                    .setFinishPrimaryWithSecondary(NEVER)
+                    .setFinishSecondaryWithPrimary(NEVER)
+                    .setClearTop(true)
+                    .setDefaultSplitAttributes(defaultSplitAttributes)
+                    .build();
             mRuleController.addRule(rule);
         }
 
-        Set<ActivityFilter> activityFilters = new HashSet<>();
-        activityFilters.add(new ActivityFilter(componentName(SplitActivityB.class), null));
-        Intent intent = new Intent();
-        intent.setComponent(
-                componentName("androidx.window.sample.embedding.SplitActivityPlaceholder"));
-        SplitPlaceholderRule placeholderRule = new SplitPlaceholderRule.Builder(
-                activityFilters,
-                intent
-        )
-                .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
-                .setMinSmallestWidthDp(0)
-                .setSticky(mViewBinding.useStickyPlaceholderCheckBox.isChecked())
-                .setFinishPrimaryWithPlaceholder(SplitRule.FINISH_ADJACENT)
-                .setSplitRatio(SPLIT_RATIO)
-                .build();
         if (mViewBinding.usePlaceholderCheckBox.isChecked()) {
-            mRuleController.addRule(placeholderRule);
+            // Split B with placeholder.
+            final Set<ActivityFilter> activityFilters = new HashSet<>();
+            activityFilters.add(new ActivityFilter(componentName(SplitActivityB.class), null));
+            final Intent intent = new Intent();
+            intent.setComponent(componentName(SplitActivityPlaceholder.class));
+            final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(
+                    activityFilters,
+                    intent
+            )
+                    .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
+                    .setMinHeightDp(0)
+                    .setMinSmallestWidthDp(0)
+                    .setSticky(mViewBinding.useStickyPlaceholderCheckBox.isChecked())
+                    .setFinishPrimaryWithPlaceholder(ADJACENT)
+                    .setDefaultSplitAttributes(defaultSplitAttributes)
+                    .build();
+            mRuleController.addRule(rule);
         }
 
-        pairFilters = new HashSet<>();
-        pairFilters.add(new SplitPairFilter(componentName(SplitActivityB.class),
-                componentName(SplitActivityC.class), null));
-        rule = new SplitPairRule.Builder(pairFilters)
-                .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
-                .setMinSmallestWidthDp(0)
-                .setFinishPrimaryWithSecondary(
-                        mViewBinding.finishBCCheckBox.isChecked()
-                                ? SplitRule.FINISH_ALWAYS : SplitRule.FINISH_NEVER
-                )
-                .setFinishSecondaryWithPrimary(
-                        mViewBinding.finishBCCheckBox.isChecked()
-                                ? SplitRule.FINISH_ALWAYS : SplitRule.FINISH_NEVER
-                )
-                .setClearTop(true)
-                .setSplitRatio(SPLIT_RATIO)
-                .build();
         if (mViewBinding.splitBCCheckBox.isChecked()) {
+            // Split B with C.
+            final Set<SplitPairFilter> pairFilters = new HashSet<>();
+            pairFilters.add(new SplitPairFilter(componentName(SplitActivityB.class),
+                    componentName(SplitActivityC.class), null));
+            final SplitPairRule rule = new SplitPairRule.Builder(pairFilters)
+                    .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
+                    .setMinHeightDp(0)
+                    .setMinSmallestWidthDp(0)
+                    .setFinishPrimaryWithSecondary(
+                            mViewBinding.finishBCCheckBox.isChecked() ? ALWAYS : NEVER
+                    )
+                    .setFinishSecondaryWithPrimary(
+                            mViewBinding.finishBCCheckBox.isChecked() ? ALWAYS : NEVER
+                    )
+                    .setClearTop(true)
+                    .setDefaultSplitAttributes(defaultSplitAttributes)
+                    .build();
             mRuleController.addRule(rule);
         }
 
-        pairFilters = new HashSet<>();
-        pairFilters.add(new SplitPairFilter(componentName("androidx.window.*"),
-                componentName(SplitActivityF.class), null));
-        rule = new SplitPairRule.Builder(pairFilters)
-                .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
-                .setMinSmallestWidthDp(0)
-                .setFinishPrimaryWithSecondary(SplitRule.FINISH_NEVER)
-                .setFinishSecondaryWithPrimary(SplitRule.FINISH_NEVER)
-                .setClearTop(true)
-                .setSplitRatio(SPLIT_RATIO)
-                .build();
         if (mViewBinding.splitWithFCheckBox.isChecked()) {
+            // Split any activity with F.
+            final Set<SplitPairFilter> pairFilters = new HashSet<>();
+            pairFilters.add(new SplitPairFilter(new ComponentName("*", "*"),
+                    componentName(SplitActivityF.class), null));
+            final SplitPairRule rule = new SplitPairRule.Builder(pairFilters)
+                    .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
+                    .setMinHeightDp(0)
+                    .setMinSmallestWidthDp(0)
+                    .setFinishPrimaryWithSecondary(NEVER)
+                    .setFinishSecondaryWithPrimary(NEVER)
+                    .setClearTop(true)
+                    .setDefaultSplitAttributes(defaultSplitAttributes)
+                    .build();
             mRuleController.addRule(rule);
         }
 
-        activityFilters = new HashSet<>();
-        activityFilters.add(new ActivityFilter(componentName(SplitActivityE.class), null));
-        ActivityRule activityRule = new ActivityRule.Builder(activityFilters)
-                .setAlwaysExpand(true)
-                .build();
         if (mViewBinding.fullscreenECheckBox.isChecked()) {
+            // Launch E in fullscreen.
+            final Set<ActivityFilter> activityFilters = new HashSet<>();
+            activityFilters.add(new ActivityFilter(componentName(SplitActivityE.class), null));
+            final ActivityRule activityRule = new ActivityRule.Builder(activityFilters)
+                    .setAlwaysExpand(true)
+                    .build();
             mRuleController.addRule(activityRule);
         }
+
+        // Always expand the dialog activity.
+        final Set<ActivityFilter> dialogActivityFilters = new HashSet<>();
+        dialogActivityFilters.add(new ActivityFilter(componentName(
+                ExpandedDialogActivity.class), null));
+        mRuleController.addRule(new ActivityRule.Builder(dialogActivityFilters)
+                .setAlwaysExpand(true)
+                .build());
     }
 
     ComponentName componentName(Class<? extends Activity> activityClass) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityC.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
similarity index 92%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityC.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
index 413c6c3..dcbbb3b 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityC.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityC.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 open class SplitActivityC : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityD.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
similarity index 92%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityD.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
index 9f919f0..a9ec11a 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityD.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityD.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 open class SplitActivityD : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityDetail.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
similarity index 95%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityDetail.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
index 6c4897e..5db4314 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityDetail.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityDetail.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.content.Intent
 import android.graphics.Color
@@ -22,7 +22,7 @@
 import android.view.View
 import android.widget.TextView
 import androidx.appcompat.app.AppCompatActivity
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 open class SplitActivityDetail : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityE.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
similarity index 92%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityE.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
index 9960ef2..17c43b1 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityE.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityE.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 open class SplitActivityE : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityF.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
similarity index 92%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityF.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
index 3243e5d..325524e 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityF.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityF.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
-import androidx.window.sample.R
+import androidx.window.demo.R
 
 open class SplitActivityF : SplitActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
similarity index 88%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
index cb64edf..49e119a 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityList.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityList.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.content.Intent
 import android.graphics.Color
@@ -26,12 +26,12 @@
 import androidx.core.util.Consumer
 import androidx.window.embedding.SplitController
 import androidx.window.embedding.SplitInfo
-import androidx.window.sample.R
-import androidx.window.sample.embedding.SplitActivityDetail.Companion.EXTRA_SELECTED_ITEM
+import androidx.window.demo.R
+import androidx.window.demo.embedding.SplitActivityDetail.Companion.EXTRA_SELECTED_ITEM
 
 open class SplitActivityList : AppCompatActivity() {
-    private lateinit var splitController: SplitController
-    private val splitChangeListener = SplitStateChangeListener()
+    lateinit var splitController: SplitController
+    val splitChangeListener = SplitStateChangeListener()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityListPlaceholder.kt
similarity index 93%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityListPlaceholder.kt
index 754e11d8..ab08fe7 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityListPlaceholder.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 open class SplitActivityListPlaceholder : SplitActivityPlaceholder()
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityPlaceholder.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
similarity index 89%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityPlaceholder.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
index 7314921..380b5b7 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityPlaceholder.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityPlaceholder.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
-import androidx.window.sample.databinding.ActivitySplitActivityPlaceholderLayoutBinding
+import androidx.window.demo.databinding.ActivitySplitActivityPlaceholderLayoutBinding
 
 open class SplitActivityPlaceholder : AppCompatActivity() {
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityTrampoline.kt
similarity index 69%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityTrampoline.kt
index 212cbcb..3732156 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityTrampoline.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitActivityTrampoline.kt
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.content.Intent
 import android.os.Bundle
 import androidx.window.embedding.ActivityFilter
 import androidx.window.embedding.RuleController
+import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitPlaceholderRule
-import androidx.window.embedding.SplitRule.Companion.FINISH_ADJACENT
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ADJACENT
 
 /**
  * Example trampoline activity that launches a split and finishes itself.
@@ -31,20 +32,24 @@
         super.onCreate(savedInstanceState)
 
         val activityFilters = setOf(ActivityFilter(componentName(
-            "androidx.window.sample.embedding.SplitActivityTrampolineTarget"), null))
+            "androidx.window.demo.embedding.SplitActivityTrampolineTarget"), null))
         val placeholderIntent = Intent()
         placeholderIntent.component =
-            componentName("androidx.window.sample.embedding.SplitActivityPlaceholder")
+            componentName("androidx.window.demo.embedding.SplitActivityPlaceholder")
+        val defaultSplitAttributes = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(SPLIT_RATIO))
+            .build()
         val placeholderRule = SplitPlaceholderRule.Builder(activityFilters, placeholderIntent)
             .setMinWidthDp(MIN_SPLIT_WIDTH_DP)
+            .setMinHeightDp(0)
             .setMinSmallestWidthDp(0)
-            .setFinishPrimaryWithPlaceholder(FINISH_ADJACENT)
-            .setSplitRatio(SPLIT_RATIO)
+            .setFinishPrimaryWithPlaceholder(ADJACENT)
+            .setDefaultSplitAttributes(defaultSplitAttributes)
             .build()
         RuleController.getInstance(this).addRule(placeholderRule)
         val activityIntent = Intent()
         activityIntent.component = componentName(
-            "androidx.window.sample.embedding.SplitActivityTrampolineTarget")
+            "androidx.window.demo.embedding.SplitActivityTrampolineTarget")
         startActivity(activityIntent)
 
         finish()
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityA.kt
similarity index 85%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
copy to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityA.kt
index 7053e2d..9ba2ac3 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityA.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
-open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
+class SplitDeviceStateActivityA : SplitDeviceStateActivityBase()
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityC.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityB.kt
similarity index 76%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityC.kt
copy to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityB.kt
index 413c6c3..6711d76 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityC.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityB.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
 import android.view.View
-import androidx.window.sample.R
+import androidx.window.demo.R
 
-open class SplitActivityC : SplitActivityBase() {
+class SplitDeviceStateActivityB : SplitDeviceStateActivityBase() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         findViewById<View>(R.id.root_split_activity_layout)
-            .setBackgroundColor(Color.parseColor("#e8f5e9"))
+            .setBackgroundColor(Color.parseColor("#fff3e0"))
     }
-}
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..b98ceb1
--- /dev/null
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitDeviceStateActivityBase.kt
@@ -0,0 +1,418 @@
+/*
+ * 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.window.demo.embedding
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Color
+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 androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.util.Consumer
+import androidx.window.embedding.EmbeddingRule
+import androidx.window.embedding.SplitAttributes
+import androidx.window.embedding.SplitController
+import androidx.window.embedding.SplitInfo
+import androidx.window.embedding.SplitPairFilter
+import androidx.window.embedding.SplitPairRule
+import androidx.window.demo.R
+import androidx.window.demo.databinding.ActivitySplitDeviceStateLayoutBinding
+import androidx.window.embedding.RuleController
+
+open class SplitDeviceStateActivityBase : AppCompatActivity(), View.OnClickListener,
+    RadioGroup.OnCheckedChangeListener, CompoundButton.OnCheckedChangeListener,
+    AdapterView.OnItemSelectedListener {
+
+    private lateinit var splitController: SplitController
+    private lateinit var ruleController: RuleController
+
+    private val splitStateChangeListener = SplitStateChangeListener()
+
+    private lateinit var splitPairRule: SplitPairRule
+    private var shouldReverseContainerPosition = false
+    private var shouldShowHorizontalInTabletop = false
+    private var shouldShowFullscreenInBookMode = false
+
+    private lateinit var viewBinding: ActivitySplitDeviceStateLayoutBinding
+    private lateinit var activityA: ComponentName
+    private lateinit var activityB: ComponentName
+
+    /** Controller to manage the global configuration. */
+    private val demoActivityEmbeddingController = DemoActivityEmbeddingController.getInstance()
+
+    /** The last selected split rule id. */
+    private var lastCheckedRuleId = 0
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        viewBinding = ActivitySplitDeviceStateLayoutBinding.inflate(layoutInflater)
+        splitController = SplitController.getInstance(this)
+        ruleController = RuleController.getInstance(this)
+
+        setContentView(viewBinding.root)
+
+        activityA = ComponentName(this, SplitDeviceStateActivityA::class.java.name)
+        activityB = ComponentName(this, SplitDeviceStateActivityB::class.java.name)
+
+        val radioGroup = viewBinding.splitAttributesOptionsRadioGroup
+        val animationBgColorDropdown = viewBinding.animationBackgroundColorDropdown
+        if (componentName == activityA) {
+            // Set to the first option
+            demoActivityEmbeddingController.animationBackgroundColor =
+                ANIMATION_BACKGROUND_COLORS_VALUE[0]
+            radioGroup.check(R.id.use_default_split_attributes)
+            onCheckedChanged(radioGroup, radioGroup.checkedRadioButtonId)
+            radioGroup.setOnCheckedChangeListener(this)
+            animationBgColorDropdown.adapter = ArrayAdapter(
+                this,
+                android.R.layout.simple_spinner_dropdown_item,
+                ANIMATION_BACKGROUND_COLORS_TEXT
+            )
+            animationBgColorDropdown.onItemSelectedListener = this
+        } else {
+            // Only update split pair rule on the primary Activity. The secondary Activity can only
+            // finish itself to prevent confusing users. We only apply the rule when the Activity is
+            // launched from the primary.
+            viewBinding.chooseLayoutTextView.visibility = View.GONE
+            radioGroup.visibility = View.GONE
+            animationBgColorDropdown.visibility = View.GONE
+            viewBinding.launchActivityToSide.text = "Finish this Activity"
+        }
+
+        viewBinding.showHorizontalLayoutInTabletopCheckBox.setOnCheckedChangeListener(this)
+        viewBinding.showFullscreenInBookModeCheckBox.setOnCheckedChangeListener(this)
+        viewBinding.swapPrimarySecondaryPositionCheckBox.setOnCheckedChangeListener(this)
+        viewBinding.launchActivityToSide.setOnClickListener(this)
+
+        val isCallbackSupported = splitController.isSplitAttributesCalculatorSupported()
+        if (!isCallbackSupported) {
+            // Disable the radioButtons that use SplitAttributesCalculator
+            viewBinding.showFullscreenInPortraitRadioButton.isEnabled = false
+            viewBinding.showHorizontalLayoutInTabletopRadioButton.isEnabled = false
+            viewBinding.showDifferentLayoutWithSizeRadioButton.isEnabled = false
+            viewBinding.splitByHingeWhenSeparatingRadioButton.isEnabled = false
+            hideAllSubCheckBoxes()
+            // Add the error message to notify the SplitAttributesCalculator is not available.
+            viewBinding.errorMessageTextView.text = "SplitAttributesCalculator is not supported!"
+            animationBgColorDropdown.isEnabled = false
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        splitController.addSplitListener(
+            this,
+            ContextCompat.getMainExecutor(this),
+            splitStateChangeListener
+        )
+    }
+
+    override fun onStop() {
+        super.onStop()
+        splitController.removeSplitListener(splitStateChangeListener)
+    }
+
+    override fun onClick(button: View) {
+        if (button.id != R.id.launch_activity_to_side) {
+            return
+        }
+        when (componentName) {
+            activityA -> {
+                startActivity(Intent(this, SplitDeviceStateActivityB::class.java))
+            }
+            activityB -> finish()
+        }
+    }
+
+    override fun onCheckedChanged(c: CompoundButton, isChecked: Boolean) {
+        when (c.id) {
+            R.id.swap_primary_secondary_position_check_box -> {
+                shouldReverseContainerPosition = isChecked
+                updateSplitPairRuleWithRadioButtonId(
+                    viewBinding.splitAttributesOptionsRadioGroup.checkedRadioButtonId
+                )
+            }
+            R.id.show_horizontal_layout_in_tabletop_check_box -> {
+                shouldShowHorizontalInTabletop = isChecked
+                updateSplitPairRuleWithRadioButtonId(
+                    R.id.show_fullscreen_in_portrait_radio_button
+                )
+            }
+            R.id.show_fullscreen_in_book_mode_check_box -> {
+                shouldShowFullscreenInBookMode = isChecked
+                updateSplitPairRuleWithRadioButtonId(
+                    R.id.show_different_layout_with_size_radio_button
+                )
+            }
+        }
+    }
+
+    override fun onCheckedChanged(group: RadioGroup, id: Int) {
+        updateCheckboxWithRadioButton(id)
+        updateSplitPairRuleWithRadioButtonId(id)
+    }
+
+    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+        demoActivityEmbeddingController.animationBackgroundColor =
+            ANIMATION_BACKGROUND_COLORS_VALUE[position]
+        updateSplitPairRuleWithRadioButtonId(lastCheckedRuleId)
+    }
+
+    override fun onNothingSelected(view: AdapterView<*>?) {
+        // Auto-generated method stub
+    }
+
+    private fun updateCheckboxWithRadioButton(id: Int) {
+        when (id) {
+            R.id.show_fullscreen_in_portrait_radio_button -> {
+                showCheckBox(R.id.show_horizontal_layout_in_tabletop_check_box)
+                hideCheckBox(R.id.show_fullscreen_in_book_mode_check_box)
+            }
+            R.id.show_different_layout_with_size_radio_button -> {
+                hideCheckBox(R.id.show_horizontal_layout_in_tabletop_check_box)
+                showCheckBox(R.id.show_fullscreen_in_book_mode_check_box)
+            }
+            else -> hideAllSubCheckBoxes()
+        }
+        // Disable the checkbox because this won't be applied if users want to use the default rule
+        // behavior.
+        viewBinding.swapPrimarySecondaryPositionCheckBox.isEnabled =
+            id != R.id.use_default_split_attributes
+    }
+
+    private fun hideAllSubCheckBoxes() {
+        hideCheckBox(R.id.show_horizontal_layout_in_tabletop_check_box)
+        hideCheckBox(R.id.show_fullscreen_in_book_mode_check_box)
+    }
+
+    /** Show check box with [id] and also hides other check boxes. */
+    private fun showCheckBox(id: Int) {
+        when (id) {
+            R.id.show_horizontal_layout_in_tabletop_check_box -> {
+                viewBinding.showFullscreenInPortraitDividerTop.visibility = View.VISIBLE
+                viewBinding.showHorizontalLayoutInTabletopCheckBox.visibility = View.VISIBLE
+                viewBinding.showFullscreenInPortraitDividerBottom.visibility = View.VISIBLE
+            }
+            R.id.show_fullscreen_in_book_mode_check_box -> {
+                viewBinding.showDifferentLayoutWithSizeDividerTop.visibility = View.VISIBLE
+                viewBinding.showFullscreenInBookModeCheckBox.visibility = View.VISIBLE
+                viewBinding.showDifferentLayoutWithSizeDividerBottom.visibility = View.VISIBLE
+            }
+        }
+    }
+
+    private fun hideCheckBox(id: Int) {
+        when (id) {
+            R.id.show_horizontal_layout_in_tabletop_check_box -> {
+                viewBinding.showFullscreenInPortraitDividerTop.visibility = View.GONE
+                viewBinding.showHorizontalLayoutInTabletopCheckBox.visibility = View.GONE
+                viewBinding.showFullscreenInPortraitDividerBottom.visibility = View.GONE
+                shouldShowHorizontalInTabletop = false
+            }
+            R.id.show_fullscreen_in_book_mode_check_box -> {
+                viewBinding.showDifferentLayoutWithSizeDividerTop.visibility = View.GONE
+                viewBinding.showFullscreenInBookModeCheckBox.visibility = View.GONE
+                viewBinding.showDifferentLayoutWithSizeDividerBottom.visibility = View.GONE
+                shouldShowFullscreenInBookMode = false
+            }
+        }
+    }
+
+    private fun updateSplitPairRuleWithRadioButtonId(id: Int) {
+        lastCheckedRuleId = id
+        ruleController.clearRules()
+
+        val splitPairFilters = HashSet<SplitPairFilter>()
+        val splitPairFilter = SplitPairFilter(
+            activityA,
+            activityB,
+            secondaryActivityIntentAction = null
+        )
+        splitPairFilters.add(splitPairFilter)
+        val defaultSplitAttributes = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.splitEqually())
+            .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+            .setAnimationBackgroundColor(demoActivityEmbeddingController.animationBackgroundColor)
+            .build()
+        // Use the tag to control the rule how to change split attributes with the current state
+        var tag = when (id) {
+            R.id.use_default_split_attributes -> TAG_USE_DEFAULT_SPLIT_ATTRIBUTES
+            R.id.show_fullscreen_in_portrait_radio_button -> {
+                if (shouldShowHorizontalInTabletop) {
+                    TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP
+                } else {
+                    TAG_SHOW_FULLSCREEN_IN_PORTRAIT
+                }
+            }
+            R.id.show_horizontal_layout_in_tabletop_radio_button -> {
+                if (shouldReverseContainerPosition) {
+                    TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP + SUFFIX_REVERSED
+                } else {
+                    TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP
+                }
+            }
+            R.id.show_different_layout_with_size_radio_button -> {
+                if (shouldShowFullscreenInBookMode) {
+                    TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE
+                } else {
+                    TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE
+                }
+            }
+            R.id.split_by_hinge_when_separating_radio_button ->
+                TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING
+            else -> null
+        }
+        if (shouldReverseContainerPosition) {
+            tag += SUFFIX_REVERSED
+        }
+
+        splitPairRule = SplitPairRule.Builder(splitPairFilters)
+            .setTag(tag)
+            .setMinWidthDp(DEFAULT_MINIMUM_WIDTH_DP)
+            .setMinSmallestWidthDp(DEFAULT_MINIMUM_WIDTH_DP)
+            .setDefaultSplitAttributes(defaultSplitAttributes)
+            .build()
+        ruleController.addRule(splitPairRule)
+    }
+
+    /** Updates split attributes when receives callback from the extension. */
+    inner class SplitStateChangeListener : Consumer<List<SplitInfo>> {
+        override fun accept(newSplitInfos: List<SplitInfo>) {
+            updateSplitAttributesText(newSplitInfos)
+            updateRadioGroupAndCheckBoxFromRule()
+        }
+    }
+
+    fun updateSplitAttributesText(newSplitInfos: List<SplitInfo>) {
+        var splitAttributes: SplitAttributes = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.expandContainers())
+            .build()
+        var suggestToFinishItself = false
+        val isCallbackSupported = splitController.isSplitAttributesCalculatorSupported()
+        // Traverse SplitInfos from the end because last SplitInfo has the highest z-order.
+        for (info in newSplitInfos.reversed()) {
+            if (info.contains(this@SplitDeviceStateActivityBase)) {
+                splitAttributes = info.splitAttributes
+                if (componentName == activityB &&
+                    splitAttributes.splitType
+                        is SplitAttributes.SplitType.ExpandContainersSplitType
+                ) {
+                    // We don't put any functionality on activity B. Suggest users to finish the
+                    // activity if it fills the host task.
+                    suggestToFinishItself = true
+                }
+                break
+            }
+        }
+        runOnUiThread {
+            viewBinding.activityPairSplitAttributesTextView.text =
+                resources.getString(R.string.current_split_attributes) + splitAttributes
+            if (!isCallbackSupported) {
+                // Don't update the error message if the callback is not supported.
+                return@runOnUiThread
+            }
+            viewBinding.errorMessageTextView.text =
+                if (suggestToFinishItself) {
+                    "Please finish the activity to try other split configurations."
+                } else {
+                    ""
+                }
+        }
+    }
+
+    fun updateRadioGroupAndCheckBoxFromRule() {
+        val splitPairRule = ruleController.getRules().firstOrNull { rule ->
+            isRuleForSplitActivityA(rule)
+        } ?: return
+        val tag = splitPairRule.tag
+        viewBinding.splitAttributesOptionsRadioGroup.check(
+            when (tag?.substringBefore(SUFFIX_REVERSED)) {
+                TAG_USE_DEFAULT_SPLIT_ATTRIBUTES -> R.id.use_default_split_attributes
+                TAG_SHOW_FULLSCREEN_IN_PORTRAIT,
+                TAG_SHOW_FULLSCREEN_IN_PORTRAIT + SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP ->
+                    R.id.show_fullscreen_in_portrait_radio_button
+                TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP,
+                TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP + SUFFIX_REVERSED ->
+                    R.id.show_horizontal_layout_in_tabletop_radio_button
+                TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE,
+                TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE + SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE ->
+                    R.id.show_different_layout_with_size_radio_button
+                TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING ->
+                    R.id.split_by_hinge_when_separating_radio_button
+                else -> 0
+            }
+        )
+        if (tag?.contains(TAG_SHOW_FULLSCREEN_IN_PORTRAIT) == true) {
+            showCheckBox(R.id.show_horizontal_layout_in_tabletop_check_box)
+            viewBinding.showHorizontalLayoutInTabletopCheckBox.isChecked =
+                tag.contains(SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP)
+        } else if (tag?.contains(TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE) == true) {
+            showCheckBox(R.id.swap_primary_secondary_position_check_box)
+            viewBinding.showFullscreenInBookModeCheckBox.isChecked =
+                tag.contains(SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE)
+        }
+
+        viewBinding.swapPrimarySecondaryPositionCheckBox.isChecked =
+            tag?.contains(SUFFIX_REVERSED) ?: false
+    }
+
+    private fun isRuleForSplitActivityA(rule: EmbeddingRule): Boolean {
+        if (rule !is SplitPairRule) {
+            return false
+        }
+        rule.filters.forEach { filter ->
+            if (filter.primaryActivityName == activityA &&
+                filter.secondaryActivityName == activityB
+            ) {
+                return true
+            }
+        }
+        return false
+    }
+
+    companion object {
+        const val TAG_USE_DEFAULT_SPLIT_ATTRIBUTES = "use_default_split_attributes"
+        const val TAG_SHOW_FULLSCREEN_IN_PORTRAIT = "show_fullscreen_in_portrait"
+        const val TAG_SHOW_HORIZONTAL_LAYOUT_IN_TABLETOP = "show_horizontal_layout_in_tabletop"
+        const val TAG_SHOW_DIFFERENT_LAYOUT_WITH_SIZE = "show_different_layout_with_size"
+        const val TAG_SHOW_LAYOUT_FOLLOWING_HINGE_WHEN_SEPARATING = "show_layout_following_hinge"
+        const val SUFFIX_REVERSED = "_reversed"
+        const val SUFFIX_AND_HORIZONTAL_LAYOUT_IN_TABLETOP = "_and_horizontal_layout_in_tabletop"
+        const val SUFFIX_AND_FULLSCREEN_IN_BOOK_MODE = "_and_fullscreen_in_book_mode"
+        val ANIMATION_BACKGROUND_COLORS_TEXT = arrayOf("BLACK", "BLUE", "GREEN", "YELLOW")
+        val ANIMATION_BACKGROUND_COLORS_VALUE = arrayOf(
+            Color.BLACK,
+            Color.BLUE,
+            Color.GREEN,
+            Color.YELLOW
+        )
+
+        /**
+         * The default minimum dimension for large screen devices.
+         *
+         * It is also the default value of [SplitPairRule.minWidthDp] and
+         * [SplitPairRule.minSmallestWidthDp] if the properties are not specified in static rule
+         * XML format.
+         */
+        const val DEFAULT_MINIMUM_WIDTH_DP = 600
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityA.kt
similarity index 93%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityA.kt
index 7053e2d..eaffe18 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityA.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityA.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 open class SplitPipActivityA : SplitPipActivityBase()
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityB.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityB.kt
similarity index 95%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityB.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityB.kt
index f47ab24..2446624 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityB.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityB.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.graphics.Color
 import android.os.Bundle
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
similarity index 87%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
index 9b76563..61c056c 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityBase.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.content.ComponentName
 import android.content.Intent
@@ -27,19 +27,21 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.ContextCompat
 import androidx.core.util.Consumer
+import androidx.window.demo.R
+import androidx.window.demo.common.util.PictureInPictureUtil
+import androidx.window.demo.databinding.ActivitySplitPipActivityLayoutBinding
 import androidx.window.embedding.ActivityFilter
 import androidx.window.embedding.EmbeddingRule
 import androidx.window.embedding.RuleController
+import androidx.window.embedding.SplitAttributes
 import androidx.window.embedding.SplitController
 import androidx.window.embedding.SplitInfo
 import androidx.window.embedding.SplitPairFilter
 import androidx.window.embedding.SplitPairRule
 import androidx.window.embedding.SplitPlaceholderRule
-import androidx.window.embedding.SplitRule
-import androidx.window.sample.R
-import androidx.window.sample.databinding.ActivitySplitPipActivityLayoutBinding
-import androidx.window.sample.util.PictureInPictureUtil.setPictureInPictureParams
-import androidx.window.sample.util.PictureInPictureUtil.startPictureInPicture
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ADJACENT
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
 
 /**
  * Sample showcase of split activity rules with picture-in-picture. Allows the user to select some
@@ -134,7 +136,7 @@
                 return
             }
             R.id.enter_pip_button -> {
-                startPictureInPicture(this, autoEnterPip)
+                PictureInPictureUtil.startPictureInPicture(this, autoEnterPip)
             }
         }
     }
@@ -159,14 +161,14 @@
                 }
             }
         }
-        setPictureInPictureParams(this, autoEnterPip)
+        PictureInPictureUtil.setPictureInPictureParams(this, autoEnterPip)
     }
 
     /** Enters PiP if enterPipOnUserLeave checkbox is checked. */
     override fun onUserLeaveHint() {
         super.onUserLeaveHint()
         if (enterPipOnUserLeave) {
-            startPictureInPicture(this, autoEnterPip)
+            PictureInPictureUtil.startPictureInPicture(this, autoEnterPip)
         }
     }
 
@@ -182,10 +184,10 @@
             viewBinding.splitMainCheckBox.isChecked = true
             viewBinding.finishPrimaryWithSecondaryCheckBox.isEnabled = true
             viewBinding.finishPrimaryWithSecondaryCheckBox.isChecked =
-                splitRule.finishPrimaryWithSecondary == SplitRule.FINISH_ALWAYS
+                splitRule.finishPrimaryWithSecondary == ALWAYS
             viewBinding.finishSecondaryWithPrimaryCheckBox.isEnabled = true
             viewBinding.finishSecondaryWithPrimaryCheckBox.isChecked =
-                splitRule.finishSecondaryWithPrimary == SplitRule.FINISH_ALWAYS
+                splitRule.finishSecondaryWithPrimary == ALWAYS
         } else {
             viewBinding.splitMainCheckBox.isChecked = false
             viewBinding.finishPrimaryWithSecondaryCheckBox.isEnabled = false
@@ -236,7 +238,9 @@
     /** Updates the split rules based on the current selection on checkboxes. */
     private fun updateSplitRules() {
         ruleController.clearRules()
-
+        val defaultSplitAttributes = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(splitRatio))
+            .build()
         if (viewBinding.splitMainCheckBox.isChecked) {
             val pairFilters = HashSet<SplitPairFilter>()
             pairFilters.add(SplitPairFilter(componentNameA, componentNameB, null))
@@ -245,13 +249,14 @@
             val finishBWithA = viewBinding.finishSecondaryWithPrimaryCheckBox.isChecked
             val rule = SplitPairRule.Builder(pairFilters)
                 .setMinWidthDp(0)
+                .setMinHeightDp(0)
                 .setMinSmallestWidthDp(0)
                 .setFinishPrimaryWithSecondary(
-                    if (finishAWithB) SplitRule.FINISH_ALWAYS else SplitRule.FINISH_NEVER)
+                    if (finishAWithB) ALWAYS else NEVER)
                 .setFinishSecondaryWithPrimary(
-                    if (finishBWithA) SplitRule.FINISH_ALWAYS else SplitRule.FINISH_NEVER)
+                    if (finishBWithA) ALWAYS else NEVER)
                 .setClearTop(true)
-                .setSplitRatio(splitRatio)
+                .setDefaultSplitAttributes(defaultSplitAttributes)
                 .build()
             ruleController.addRule(rule)
         }
@@ -263,10 +268,11 @@
             val isSticky = viewBinding.useStickyPlaceHolderCheckBox.isChecked
             val rule = SplitPlaceholderRule.Builder(activityFilters, intent)
                 .setMinWidthDp(0)
+                .setMinHeightDp(0)
                 .setMinSmallestWidthDp(0)
                 .setSticky(isSticky)
-                .setFinishPrimaryWithPlaceholder(SplitRule.FINISH_ADJACENT)
-                .setSplitRatio(splitRatio)
+                .setFinishPrimaryWithPlaceholder(ADJACENT)
+                .setDefaultSplitAttributes(defaultSplitAttributes)
                 .build()
             ruleController.addRule(rule)
         }
@@ -291,7 +297,10 @@
         override fun accept(newSplitInfos: List<SplitInfo>) {
             var isInSplit = false
             for (info in newSplitInfos) {
-                if (info.contains(this@SplitPipActivityBase) && info.splitRatio > 0) {
+                if (info.contains(this@SplitPipActivityBase) &&
+                    info.splitAttributes.splitType !is
+                        SplitAttributes.SplitType.ExpandContainersSplitType
+                ) {
                     isInSplit = true
                     break
                 }
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityNoPip.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityNoPip.kt
similarity index 95%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityNoPip.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityNoPip.kt
index 3cfc28c..0af97fb 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityNoPip.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityNoPip.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 import android.os.Bundle
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityPlaceholder.kt b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityPlaceholder.kt
similarity index 93%
rename from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityPlaceholder.kt
rename to window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityPlaceholder.kt
index 7c8639b..5989917 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityPlaceholder.kt
+++ b/window/window-demos/demo/src/main/java/androidx/window/demo/embedding/SplitPipActivityPlaceholder.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.demo.embedding
 
 open class SplitPipActivityPlaceholder : SplitActivityPlaceholder()
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/drawable/border.xml b/window/window-demos/demo/src/main/res/drawable/border.xml
similarity index 100%
rename from window/window-samples/src/main/res/drawable/border.xml
rename to window/window-demos/demo/src/main/res/drawable/border.xml
diff --git a/window/window-samples/src/main/res/drawable/ic_android_green_320dp.xml b/window/window-demos/demo/src/main/res/drawable/ic_android_green_320dp.xml
similarity index 100%
rename from window/window-samples/src/main/res/drawable/ic_android_green_320dp.xml
rename to window/window-demos/demo/src/main/res/drawable/ic_android_green_320dp.xml
diff --git a/window/window-samples/src/main/res/layout/activity_display_features_no_config_change.xml b/window/window-demos/demo/src/main/res/layout/activity_display_features_no_config_change.xml
similarity index 92%
copy from window/window-samples/src/main/res/layout/activity_display_features_no_config_change.xml
copy to window/window-demos/demo/src/main/res/layout/activity_display_features_no_config_change.xml
index 46ade36..3a86e95 100644
--- a/window/window-samples/src/main/res/layout/activity_display_features_no_config_change.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_display_features_no_config_change.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2021 The Android Open Source Project
+  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.
@@ -22,7 +22,7 @@
     android:id="@+id/rootLayout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="androidx.window.sample.DisplayFeaturesNoConfigChangeActivity">
+    tools:context="androidx.window.demo.DisplayFeaturesActivity">
 
     <FrameLayout
         android:id="@+id/feature_container_layout"
@@ -38,7 +38,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:text="@string/current_state"
-        android:textAppearance="@style/TextAppearance.AppCompat.Large"
+        android:textAppearance="@style/TextAppearance.AppCompat"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.0"
         app:layout_constraintStart_toStartOf="parent"
@@ -87,7 +87,7 @@
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/current_state"
+        app:layout_constraintTop_toBottomOf="@id/current_state"
         app:layout_constraintBottom_toBottomOf="parent"/>
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_foldin.xml b/window/window-demos/demo/src/main/res/layout/activity_foldin.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_foldin.xml
rename to window/window-demos/demo/src/main/res/layout/activity_foldin.xml
diff --git a/window/window-samples/src/main/res/layout/activity_ime.xml b/window/window-demos/demo/src/main/res/layout/activity_ime.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_ime.xml
rename to window/window-demos/demo/src/main/res/layout/activity_ime.xml
diff --git a/window/window-samples/src/main/res/layout/activity_organized_test_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_organized_test_layout.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_organized_test_layout.xml
rename to window/window-demos/demo/src/main/res/layout/activity_organized_test_layout.xml
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
new file mode 100644
index 0000000..43bea60
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_rear_display.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 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.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <androidx.recyclerview.widget.RecyclerView
+        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"/>
+
+    <Button
+        android:id="@+id/rear_display_button"
+        android:text="Enable RearDisplay"
+        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" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ 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
new file mode 100644
index 0000000..0bbcddf
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_activity_layout.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/root_split_activity_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:padding="10dp">
+        <TextView
+            android:id="@+id/activity_embedded_status_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Activity is embedded" />
+
+        <CheckBox
+            android:id="@+id/splitMainCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Split Main with other activities" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/launch_b"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch B" />
+
+        <CheckBox
+            android:id="@+id/usePlaceholderCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Use a placeholder for B" />
+
+        <CheckBox
+            android:id="@+id/useStickyPlaceholderCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Placeholder is sticky" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/launch_b_and_C"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch B and C" />
+
+        <CheckBox
+            android:id="@+id/splitBCCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Split B with C" />
+
+        <CheckBox
+            android:id="@+id/finishBCCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Finish B and C together"
+            android:enabled="false" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/launch_e"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch E" />
+
+        <CheckBox
+            android:id="@+id/fullscreenECheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Always launch E in fullscreen" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/launch_f"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch f" />
+
+        <Button
+            android:id="@+id/launch_f_pending_intent"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch F via Pending Intent" />
+
+        <CheckBox
+            android:id="@+id/splitWithFCheckBox"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Split everything with F" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:text="Second app (UID)"
+            />
+
+        <Button
+            android:id="@+id/launch_uid2_trusted"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch with known certificate" />
+
+        <Button
+            android:id="@+id/launch_uid2_untrusted"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch in untrusted mode" />
+
+        <Button
+            android:id="@+id/launch_uid2_untrusted_display_features"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch display features" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/launch_expanded_dialog_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:text="Launch Expanded Dialog" />
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_split_activity_list_detail_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_activity_list_detail_layout.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_split_activity_list_detail_layout.xml
rename to window/window-demos/demo/src/main/res/layout/activity_split_activity_list_detail_layout.xml
diff --git a/window/window-samples/src/main/res/layout/activity_split_activity_list_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_activity_list_layout.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_split_activity_list_layout.xml
rename to window/window-demos/demo/src/main/res/layout/activity_split_activity_list_layout.xml
diff --git a/window/window-samples/src/main/res/layout/activity_split_activity_placeholder_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_activity_placeholder_layout.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_split_activity_placeholder_layout.xml
rename to window/window-demos/demo/src/main/res/layout/activity_split_activity_placeholder_layout.xml
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
new file mode 100644
index 0000000..eb67379
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_device_state_layout.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/root_split_activity_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:padding="10dp">
+
+        <TextView
+            android:id="@+id/activity_pair_split_attributes_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_split_attributes"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <TextView
+            android:id="@+id/choose_layout_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Choose the configuration to update the layout:" />
+
+        <RadioGroup
+            android:id="@+id/split_attributes_options_radio_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+            <RadioButton
+                android:id="@+id/use_default_split_attributes"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Use the default split attributes"
+                android:checked="true"/>
+
+            <!-- The fullscreen option group -->
+            <View
+                android:id="@+id/show_fullscreen_in_portrait_divider_top"
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="10dp"
+                android:background="#AAAAAA"
+                android:visibility="gone"/>
+            <RadioButton
+                android:id="@+id/show_fullscreen_in_portrait_radio_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Show fullscreen in portrait"
+                android:checked="true"/>
+            <CheckBox
+                android:id="@+id/show_horizontal_layout_in_tabletop_check_box"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Also show horizontal layout in tabletop mode"
+                android:visibility="gone"/>
+            <View
+                android:id="@+id/show_fullscreen_in_portrait_divider_bottom"
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="10dp"
+                android:background="#AAAAAA"
+                android:visibility="gone"/>
+            <!-- End of the fullscreen option group -->
+
+            <RadioButton
+                android:id="@+id/show_horizontal_layout_in_tabletop_radio_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Show horizontal layout in tabletop mode"/>
+
+            <!-- The different layout option group -->
+            <View
+                android:id="@+id/show_different_layout_with_size_divider_top"
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="10dp"
+                android:background="#AAAAAA"
+                android:visibility="gone"/>
+            <RadioButton
+                android:id="@+id/show_different_layout_with_size_radio_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Show different layout with size"/>
+            <CheckBox
+                android:id="@+id/show_fullscreen_in_book_mode_check_box"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Also show fullscreen in book mode"
+                android:visibility="gone"/>
+            <View
+                android:id="@+id/show_different_layout_with_size_divider_bottom"
+                android:layout_width="match_parent"
+                android:layout_height="1dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="10dp"
+                android:background="#AAAAAA"
+                android:visibility="gone"/>
+            <!-- End of the different layout option group -->
+
+            <RadioButton
+                android:id="@+id/split_by_hinge_when_separating_radio_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Show layout that follows the hinge when it is separated by hinge"/>
+            <CheckBox
+                android:id="@+id/swap_primary_secondary_position_check_box"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="Swap the position of primary and secondary container" />
+        </RadioGroup>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <!-- Dropdown for animation background color -->
+
+        <TextView
+            android:id="@+id/animation_background_color_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/current_animation_background_color"/>
+
+        <Spinner
+            android:id="@+id/animation_background_color_dropdown"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spinnerMode="dropdown" />
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+
+        <Button
+            android:id="@+id/launch_activity_to_side"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:layout_centerHorizontal="true"
+            android:text="Launch activity to side"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="10dp"
+            android:background="#AAAAAA" />
+        <TextView
+            android:id="@+id/error_message_text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_split_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_layout.xml
similarity index 94%
rename from window/window-samples/src/main/res/layout/activity_split_layout.xml
rename to window/window-demos/demo/src/main/res/layout/activity_split_layout.xml
index 6950bf3..661df38 100644
--- a/window/window-samples/src/main/res/layout/activity_split_layout.xml
+++ b/window/window-demos/demo/src/main/res/layout/activity_split_layout.xml
@@ -24,7 +24,7 @@
     android:layout_height="match_parent"
     tools:context="SplitLayoutActivity">
 
-    <androidx.window.sample.SplitLayout
+    <androidx.window.demo.SplitLayout
         android:id="@+id/split_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -37,5 +37,5 @@
         <include
             android:id="@id/end_layout"
             layout="@layout/split_layout_control" />
-    </androidx.window.sample.SplitLayout>
+    </androidx.window.demo.SplitLayout>
 </LinearLayout>
diff --git a/window/window-samples/src/main/res/layout/activity_split_pip_activity_layout.xml b/window/window-demos/demo/src/main/res/layout/activity_split_pip_activity_layout.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_split_pip_activity_layout.xml
rename to window/window-demos/demo/src/main/res/layout/activity_split_pip_activity_layout.xml
diff --git a/window/window-samples/src/main/res/layout/activity_window_demos.xml b/window/window-demos/demo/src/main/res/layout/activity_window_demos.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_window_demos.xml
rename to window/window-demos/demo/src/main/res/layout/activity_window_demos.xml
diff --git a/window/window-samples/src/main/res/layout/activity_window_metrics.xml b/window/window-demos/demo/src/main/res/layout/activity_window_metrics.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/activity_window_metrics.xml
rename to window/window-demos/demo/src/main/res/layout/activity_window_metrics.xml
diff --git a/window/window-samples/src/main/res/layout/presentation_second_display.xml b/window/window-demos/demo/src/main/res/layout/presentation_second_display.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/presentation_second_display.xml
rename to window/window-demos/demo/src/main/res/layout/presentation_second_display.xml
diff --git a/window/window-samples/src/main/res/layout/split_layout_content.xml b/window/window-demos/demo/src/main/res/layout/split_layout_content.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/split_layout_content.xml
rename to window/window-demos/demo/src/main/res/layout/split_layout_content.xml
diff --git a/window/window-samples/src/main/res/layout/split_layout_control.xml b/window/window-demos/demo/src/main/res/layout/split_layout_control.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/split_layout_control.xml
rename to window/window-demos/demo/src/main/res/layout/split_layout_control.xml
diff --git a/window/window-demos/demo/src/main/res/layout/test_ime.xml b/window/window-demos/demo/src/main/res/layout/test_ime.xml
new file mode 100644
index 0000000..07f5691
--- /dev/null
+++ b/window/window-demos/demo/src/main/res/layout/test_ime.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@color/imeBackground">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="140dp"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/button_clear"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/test_ime_button_clear"/>
+
+        <Button
+            android:id="@+id/button_close"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/test_ime_button_close"/>
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/view_holder_demo_item.xml b/window/window-demos/demo/src/main/res/layout/view_holder_demo_item.xml
similarity index 100%
rename from window/window-samples/src/main/res/layout/view_holder_demo_item.xml
rename to window/window-demos/demo/src/main/res/layout/view_holder_demo_item.xml
diff --git a/window/window-samples/src/main/res/menu/picture_in_picture_menu.xml b/window/window-demos/demo/src/main/res/menu/picture_in_picture_menu.xml
similarity index 100%
rename from window/window-samples/src/main/res/menu/picture_in_picture_menu.xml
rename to window/window-demos/demo/src/main/res/menu/picture_in_picture_menu.xml
diff --git a/window/window-samples/src/main/res/values/attrs.xml b/window/window-demos/demo/src/main/res/values/attrs.xml
similarity index 100%
rename from window/window-samples/src/main/res/values/attrs.xml
rename to window/window-demos/demo/src/main/res/values/attrs.xml
diff --git a/window/window-samples/src/main/res/values/colors.xml b/window/window-demos/demo/src/main/res/values/colors.xml
similarity index 95%
rename from window/window-samples/src/main/res/values/colors.xml
rename to window/window-demos/demo/src/main/res/values/colors.xml
index 41a72b2..95a6cfe 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/window/window-demos/demo/src/main/res/values/colors.xml
@@ -24,4 +24,6 @@
 
     <color name="colorSplitContentBackground">#3B6BDB4C</color>
     <color name="colorSplitControlsBackground">#475ABFF3</color>
+
+    <color name="imeBackground">#EEEEEE</color>
 </resources>
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-demos/demo/src/main/res/values/strings.xml
similarity index 86%
rename from window/window-samples/src/main/res/values/strings.xml
rename to window/window-demos/demo/src/main/res/values/strings.xml
index 45e3de2..ae441a4 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-demos/demo/src/main/res/values/strings.xml
@@ -16,9 +16,7 @@
 
 <resources>
     <string name="app_name">WindowSamples</string>
-    <string name="current_state">Current state</string>
     <string name="deviceState">Device state</string>
-    <string name="window_layout">Window layout</string>
     <string name="fold">Fold</string>
     <string name="legend">Legend:</string>
     <string name="content_title">Content title</string>
@@ -43,15 +41,14 @@
     <string name="show_all_display_features_no_config_change_description">Show all display features of the device on the screen and do not handle config changes.  The activity is recreated instead on rotation or resize</string>
     <string name="split_layout_demo_description">Demo of a layout that splits the content to sides of a fold or a hinge. If not present or minimal size requirements are not meant, it behave like a FrameLayout.</string>
     <string name="presentation_demo_description">Demo of using Presentation API to show content on secondary display.</string>
-    <string name="screens_are_separated">"Screens are separated"</string>
-    <string name="screens_are_not_separated">"Screen is not separated"</string>
-    <string name="screen_is_horizontal">"Hinge is horizontal"</string>
-    <string name="screen_is_vertical">"Hinge is vertical"</string>
-    <string name="occlusion_is_full">Occlusion is full</string>
-    <string name="occlusion_is_none">Occlusion is none</string>
     <string name="window_metrics">Window metrics</string>
     <string name="window_metrics_description">Demo of using WindowMetrics API with activity handling rotations.</string>
+    <string name="rear_display">Rear Display Mode</string>
+    <string name="rear_display_description">Demo of observing to WindowAreaStatus and enabling/disabling RearDisplay mode</string>
+    <string name="current_split_attributes">Current SplitAttributes:</string>>
+    <string name="current_animation_background_color">Current Animation Background Color:</string>>
     <string name="test_ime">Test IME</string>
+    <string name="test_ime_button_clear">Clear Logs</string>
     <string name="test_ime_button_close">Close Test IME</string>
     <string name="window_metrics_ime_hint">Tap to open IME</string>
     <string name="ime">IME</string>
@@ -59,4 +56,5 @@
     <string name="ime_demo_reminder">Reminder: To use the Test IME bundled with this application, remember to enable it in System Settings.</string>
     <string name="ime_button_settings">System IME Settings</string>
     <string name="ime_button_switch_default">Switch default IME</string>
+    <string name="install_samples_2">Install window-demos:demo-second-app to launch activities from a different UID.</string>
 </resources>
diff --git a/window/window-samples/src/main/res/values/styles.xml b/window/window-demos/demo/src/main/res/values/styles.xml
similarity index 78%
copy from window/window-samples/src/main/res/values/styles.xml
copy to window/window-demos/demo/src/main/res/values/styles.xml
index eaa9ab2..5586114 100644
--- a/window/window-samples/src/main/res/values/styles.xml
+++ b/window/window-demos/demo/src/main/res/values/styles.xml
@@ -24,4 +24,9 @@
         <item name="colorAccent">@color/colorAccent</item>
     </style>
 
+    <!-- Theme to show the expanded dialog Activity as transparent. -->
+    <style name="ExpandedDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
+        <item name="windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+    </style>
 </resources>
diff --git a/window/window-samples/src/main/res/xml/main_split_config.xml b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
similarity index 73%
rename from window/window-samples/src/main/res/xml/main_split_config.xml
rename to window/window-demos/demo/src/main/res/xml/main_split_config.xml
index 5b93a1c..e4b150d 100644
--- a/window/window-samples/src/main/res/xml/main_split_config.xml
+++ b/window/window-demos/demo/src/main/res/xml/main_split_config.xml
@@ -20,14 +20,14 @@
         window:finishPrimaryWithSecondary="always"
         window:finishSecondaryWithPrimary="adjacent">
         <SplitPairFilter
-            window:primaryActivityName="androidx.window.sample.embedding.SplitActivityList"
-            window:secondaryActivityName="androidx.window.sample.embedding.SplitActivityDetail"/>
+            window:primaryActivityName="androidx.window.demo.embedding.SplitActivityList"
+            window:secondaryActivityName="androidx.window.demo.embedding.SplitActivityDetail"/>
     </SplitPairRule>
     <SplitPlaceholderRule
-        window:placeholderActivityName="androidx.window.sample.embedding.SplitActivityListPlaceholder"
+        window:placeholderActivityName="androidx.window.demo.embedding.SplitActivityListPlaceholder"
         window:stickyPlaceholder="true"
         window:finishPrimaryWithSecondary="adjacent">
         <ActivityFilter
-            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+            window:activityName="androidx.window.demo.embedding.SplitActivityList"/>
     </SplitPlaceholderRule>
 </resources>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/xml/method.xml b/window/window-demos/demo/src/main/res/xml/method.xml
similarity index 100%
rename from window/window-samples/src/main/res/xml/method.xml
rename to window/window-demos/demo/src/main/res/xml/method.xml
diff --git a/window/window-java/api/public_plus_experimental_current.txt b/window/window-java/api/public_plus_experimental_current.txt
index 709904b..a56988b 100644
--- a/window/window-java/api/public_plus_experimental_current.txt
+++ b/window/window-java/api/public_plus_experimental_current.txt
@@ -1,4 +1,15 @@
 // Signature format: 4.0
+package androidx.window.java.area {
+
+  @androidx.window.core.ExperimentalWindowApi public final class WindowAreaControllerJavaAdapter implements androidx.window.area.WindowAreaController {
+    ctor public WindowAreaControllerJavaAdapter(androidx.window.area.WindowAreaController controller);
+    method public void addRearDisplayStatusListener(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
+    method public void removeRearDisplayStatusListener(androidx.core.util.Consumer<androidx.window.area.WindowAreaStatus> consumer);
+    method public void startRearDisplayModeSession(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.window.area.WindowAreaSessionCallback windowAreaSessionCallback);
+  }
+
+}
+
 package androidx.window.java.layout {
 
   public final class WindowInfoTrackerCallbackAdapter implements androidx.window.layout.WindowInfoTracker {
diff --git a/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
new file mode 100644
index 0000000..584445f
--- /dev/null
+++ b/window/window-java/src/main/java/androidx/window/java/area/WindowAreaControllerJavaAdapter.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 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.window.java.area
+
+import android.app.Activity
+import androidx.core.util.Consumer
+import androidx.window.area.WindowAreaSessionCallback
+import androidx.window.area.WindowAreaStatus
+import androidx.window.area.WindowAreaController
+import androidx.window.core.ExperimentalWindowApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * An adapted interface for [WindowAreaController] that provides the information and
+ * functionality around RearDisplay Mode via a callback shaped API.
+ */
+@ExperimentalWindowApi
+class WindowAreaControllerJavaAdapter(
+    private val controller: WindowAreaController
+) : WindowAreaController by controller {
+
+    /**
+     * A [ReentrantLock] to protect against concurrent access to [consumerToJobMap].
+     */
+    private val lock = ReentrantLock()
+    private val consumerToJobMap = mutableMapOf<Consumer<*>, Job>()
+
+    /**
+     * Registers a listener to consume [WindowAreaStatus] values defined as
+     * [WindowAreaStatus.UNSUPPORTED], [WindowAreaStatus.UNAVAILABLE], and
+     * [WindowAreaStatus.AVAILABLE]. The values provided through this listener should be used
+     * to determine if you are able to enable rear display Mode at that time. You can use these
+     * values to modify your UI to show/hide controls and determine when to enable features
+     * that use rear display Mode. You should only try and enter rear display mode when your
+     * [consumer] is provided a value of [WindowAreaStatus.AVAILABLE].
+     *
+     * The [consumer] will be provided an initial value on registration, as well as any change
+     * to the status as they occur. This could happen due to hardware device state changes, or if
+     * another process has enabled RearDisplay Mode.
+     *
+     * @see WindowAreaController.rearDisplayStatus
+     */
+    fun addRearDisplayStatusListener(
+        executor: Executor,
+        consumer: Consumer<WindowAreaStatus>
+    ) {
+        val statusFlow = controller.rearDisplayStatus()
+        lock.withLock {
+            if (consumerToJobMap[consumer] == null) {
+                val scope = CoroutineScope(executor.asCoroutineDispatcher())
+                consumerToJobMap[consumer] = scope.launch {
+                    statusFlow.collect { consumer.accept(it) }
+                }
+            }
+        }
+    }
+
+    /**
+     * Removes a listener of [WindowAreaStatus] values
+     * @see WindowAreaController.rearDisplayStatus
+     */
+    fun removeRearDisplayStatusListener(consumer: Consumer<WindowAreaStatus>) {
+        lock.withLock {
+            consumerToJobMap[consumer]?.cancel()
+            consumerToJobMap.remove(consumer)
+        }
+    }
+
+    /**
+     * Starts a RearDisplay Mode session and provides updates through the
+     * [WindowAreaSessionCallback] provided. Due to the nature of moving your Activity to a
+     * different display, your Activity will likely go through a configuration change. Because of
+     * this, if your Activity does not override configuration changes, this method should be called
+     * from a component that outlives the Activity lifecycle such as a
+     * [androidx.lifecycle.ViewModel]. If your Activity does override
+     * configuration changes, it is safe to call this method inside your Activity.
+     *
+     * This method should only be called if you have received a [WindowAreaStatus.AVAILABLE]
+     * value from the listener provided through the [addRearDisplayStatusListener] method. If
+     * you try and enable RearDisplay mode without it being available, you will receive an
+     * [UnsupportedOperationException].
+     *
+     * The [windowAreaSessionCallback] provided will receive a call to
+     * [WindowAreaSessionCallback.onSessionStarted] after your Activity has been moved to the
+     * display corresponding to this mode. RearDisplay mode will stay active until the session
+     * provided through [WindowAreaSessionCallback.onSessionStarted] is closed, or there is a device
+     * state change that makes RearDisplay mode incompatible such as if the device is closed so the
+     * outer-display is no longer in line with the rear camera. When this occurs,
+     * [WindowAreaSessionCallback.onSessionEnded] is called to notify you the session has been
+     * ended.
+     *
+     * @see addRearDisplayStatusListener
+     * @throws UnsupportedOperationException if you try and start a RearDisplay session when
+     * your [WindowAreaController.rearDisplayStatus] does not return a value of
+     * [WindowAreaStatus.AVAILABLE]
+     */
+    fun startRearDisplayModeSession(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    ) {
+        controller.rearDisplayMode(activity, executor, windowAreaSessionCallback)
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesConfigChangeActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesConfigChangeActivity.kt
deleted file mode 100644
index 7f07d24..0000000
--- a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesConfigChangeActivity.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.sample
-
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.widget.FrameLayout
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.RecyclerView
-import androidx.window.layout.WindowInfoTracker
-import androidx.window.layout.WindowLayoutInfo
-import androidx.window.sample.infolog.InfoLogAdapter
-import androidx.window.sample.util.PictureInPictureUtil.appendPictureInPictureMenu
-import androidx.window.sample.util.PictureInPictureUtil.handlePictureInPictureMenuItem
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-
-/** Demo activity that shows all display features and current device state on the screen. */
-class DisplayFeaturesConfigChangeActivity : AppCompatActivity() {
-
-    private val infoLogAdapter = InfoLogAdapter()
-    private val displayFeatureViews = ArrayList<View>()
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_display_features_config_change)
-        val recyclerView = findViewById<RecyclerView>(R.id.infoLogRecyclerView)
-        recyclerView.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
-                WindowInfoTracker.getOrCreate(this@DisplayFeaturesConfigChangeActivity)
-                    .windowLayoutInfo(this@DisplayFeaturesConfigChangeActivity)
-                    .collect { newLayoutInfo ->
-                        // New posture information
-                        updateStateLog(newLayoutInfo)
-                        updateCurrentState(newLayoutInfo)
-                    }
-            }
-        }
-    }
-
-    override fun onCreateOptionsMenu(menu: Menu): Boolean {
-        appendPictureInPictureMenu(menuInflater, menu)
-        return true
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        return when {
-            handlePictureInPictureMenuItem(this, item) -> true
-            else -> super.onOptionsItemSelected(item)
-        }
-    }
-
-    /** Updates the device state and display feature positions. */
-    private fun updateCurrentState(windowLayoutInfo: WindowLayoutInfo) {
-        // Cleanup previously added feature views
-        val rootLayout = findViewById<FrameLayout>(R.id.featureContainerLayout)
-        for (featureView in displayFeatureViews) {
-            rootLayout.removeView(featureView)
-        }
-        displayFeatureViews.clear()
-
-        // Add views that represent display features
-        for (displayFeature in windowLayoutInfo.displayFeatures) {
-            val lp = getLayoutParamsForFeatureInFrameLayout(displayFeature, rootLayout)
-
-            // Make sure that zero-wide and zero-high features are still shown
-            if (lp.width == 0) {
-                lp.width = 1
-            }
-            if (lp.height == 0) {
-                lp.height = 1
-            }
-
-            val featureView = View(this)
-            val color = getColor(R.color.colorFeatureFold)
-            featureView.foreground = ColorDrawable(color)
-
-            rootLayout.addView(featureView, lp)
-            featureView.id = View.generateViewId()
-
-            displayFeatureViews.add(featureView)
-        }
-    }
-
-    /** Adds the current state to the text log of changes on screen. */
-    private fun updateStateLog(info: Any) {
-        infoLogAdapter.append(getCurrentTimeString(), info.toString())
-        infoLogAdapter.notifyDataSetChanged()
-    }
-
-    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-samples/src/main/java/androidx/window/sample/TestIme.kt b/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt
deleted file mode 100644
index d337e11..0000000
--- a/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt
+++ /dev/null
@@ -1,42 +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.window.sample
-
-import android.inputmethodservice.InputMethodService
-import android.view.View
-import android.view.inputmethod.InputMethodManager
-import android.widget.Button
-
-/**
- * A test IME that currently provides a minimal UI containing a "Close" button. To use this, go to
- * "Settings > System > Languages & Input > On-screen keyboard" and enable "Test IME". Remember you
- * may still need to switch to this IME after the default on-screen keyboard pops up.
- */
-internal class TestIme : InputMethodService() {
-
-    override fun onCreateInputView(): View {
-        return layoutInflater.inflate(R.layout.test_ime, null).apply {
-            findViewById<Button>(R.id.button_close).setOnClickListener {
-                requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS)
-            }
-        }
-    }
-
-    override fun onEvaluateFullscreenMode(): Boolean {
-        return false
-    }
-}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt b/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt
deleted file mode 100644
index 56f41445..0000000
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/ExampleWindowInitializer.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 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.window.sample.embedding
-
-import android.content.Context
-import androidx.startup.Initializer
-import androidx.window.embedding.RuleController
-import androidx.window.sample.R
-
-/**
- * Initializes [RuleController] with a set of statically defined rules.
- */
-class ExampleWindowInitializer : Initializer<RuleController> {
-    override fun create(context: Context): RuleController =
-        RuleController.getInstance(context).apply {
-            setRules(RuleController.parseRules(context, R.xml.main_split_config))
-        }
-
-    override fun dependencies(): List<Class<out Initializer<*>>> {
-        return emptyList()
-    }
-}
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_display_features_config_change.xml b/window/window-samples/src/main/res/layout/activity_display_features_config_change.xml
deleted file mode 100644
index 0e877e4..0000000
--- a/window/window-samples/src/main/res/layout/activity_display_features_config_change.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2020 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/rootLayout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context="androidx.window.sample.DisplayFeaturesConfigChangeActivity">
-
-    <FrameLayout
-        android:id="@+id/featureContainerLayout"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
-    <LinearLayout
-        android:id="@+id/legendLayout"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent">
-
-        <TextView
-            android:id="@+id/legendTextView"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:text="@string/legend" />
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="horizontal">
-
-            <ImageView
-                android:id="@+id/foldColorImageView"
-                android:layout_width="20dp"
-                android:layout_height="20dp"
-                android:foreground="@color/colorFeatureFold" />
-
-            <TextView
-                android:id="@+id/foldColorTextView"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:text="@string/fold" />
-        </LinearLayout>
-
-    </LinearLayout>
-
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/infoLogRecyclerView"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"/>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_split_activity_layout.xml b/window/window-samples/src/main/res/layout/activity_split_activity_layout.xml
deleted file mode 100644
index d67eefc..0000000
--- a/window/window-samples/src/main/res/layout/activity_split_activity_layout.xml
+++ /dev/null
@@ -1,138 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2020 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/root_split_activity_layout"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:padding="10dp">
-
-    <TextView
-        android:id="@+id/activity_embedded_status_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Activity is embedded" />
-
-    <CheckBox
-        android:id="@+id/splitMainCheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Split Main with other activities" />
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_marginTop="10dp"
-        android:layout_marginBottom="10dp"
-        android:background="#AAAAAA" />
-
-    <Button
-        android:id="@+id/launch_b"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
-        android:text="Launch B" />
-
-    <CheckBox
-        android:id="@+id/usePlaceholderCheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Use a placeholder for B" />
-
-    <CheckBox
-        android:id="@+id/useStickyPlaceholderCheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Placeholder is sticky" />
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_marginTop="10dp"
-        android:layout_marginBottom="10dp"
-        android:background="#AAAAAA" />
-
-    <Button
-        android:id="@+id/launch_b_and_C"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
-        android:text="Launch B and C" />
-
-    <CheckBox
-        android:id="@+id/splitBCCheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Split B with C" />
-
-    <CheckBox
-        android:id="@+id/finishBCCheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Finish B and C together"
-        android:enabled="false" />
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_marginTop="10dp"
-        android:layout_marginBottom="10dp"
-        android:background="#AAAAAA" />
-
-    <Button
-        android:id="@+id/launch_e"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
-        android:text="Launch E" />
-
-    <CheckBox
-        android:id="@+id/fullscreenECheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Always launch E in fullscreen" />
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_marginTop="10dp"
-        android:layout_marginBottom="10dp"
-        android:background="#AAAAAA" />
-
-    <Button
-        android:id="@+id/launch_f"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
-        android:text="Launch f" />
-
-    <Button
-        android:id="@+id/launch_f_pending_intent"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:layout_centerHorizontal="true"
-        android:text="Launch F via Pending Intent" />
-
-    <CheckBox
-        android:id="@+id/splitWithFCheckBox"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="Split everything with F" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/window/window-testing/src/androidTest/java/androidx/window/testing/layout/StubWindowMetricsCalculatorRuleTest.kt b/window/window-testing/src/androidTest/java/androidx/window/testing/layout/StubWindowMetricsCalculatorRuleTest.kt
index 2118436..3e6544d 100644
--- a/window/window-testing/src/androidTest/java/androidx/window/testing/layout/StubWindowMetricsCalculatorRuleTest.kt
+++ b/window/window-testing/src/androidTest/java/androidx/window/testing/layout/StubWindowMetricsCalculatorRuleTest.kt
@@ -16,6 +16,11 @@
 
 package androidx.window.testing.layout
 
+import android.content.Context
+import android.graphics.Point
+import android.os.Build
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.window.core.ExperimentalWindowApi
@@ -75,6 +80,81 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.R)
+    @Test
+    fun testCurrentWindowMetrics_context_matchesWindowMetricsMetrics_30AndAbove() {
+        Utils.assumePlatformAtOrAbove(Build.VERSION_CODES.R)
+
+        activityRule.scenario.onActivity { activity ->
+            val calculator = WindowMetricsCalculator.getOrCreate()
+            val wm = activity.getSystemService(WindowManager::class.java)
+            val windowMetrics = wm.currentWindowMetrics.bounds
+            val actual = calculator.computeCurrentWindowMetrics(activity as Context)
+
+            assertEquals(0, actual.bounds.left)
+            assertEquals(0, actual.bounds.top)
+            assertEquals(windowMetrics.width(), actual.bounds.right)
+            assertEquals(windowMetrics.height(), actual.bounds.bottom)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @Test
+    fun testCurrentWindowMetrics_context_matchesDisplayRealSize_17to29() {
+        Utils.assumePlatformAtOrBelow(Build.VERSION_CODES.Q)
+        Utils.assumePlatformAtOrAbove(Build.VERSION_CODES.JELLY_BEAN_MR1)
+
+        activityRule.scenario.onActivity { activity ->
+            val calculator = WindowMetricsCalculator.getOrCreate()
+            val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+            val displaySize = Point()
+            // DefaultDisplay#getRealSize is used in StubWindowMetricsCalculator for compatibility
+            // with older versions. We're just asserting that the value via
+            // StubWindowMetricsCalculator#computeCurrentWindowMetrics is equal to this.
+            @Suppress("DEPRECATION")
+            wm.defaultDisplay.getRealSize(displaySize)
+            val actual = calculator.computeCurrentWindowMetrics(activity as Context)
+
+            assertEquals(0, actual.bounds.left)
+            assertEquals(0, actual.bounds.top)
+            assertEquals(displaySize.x, actual.bounds.right)
+            assertEquals(displaySize.y, actual.bounds.bottom)
+        }
+    }
+
+    // DefaultDisplay width/height used in tests for API16 and lower
+    @Suppress("DEPRECATION")
+    @Test
+    fun testCurrentWindowMetrics_context_matchesDisplayMetrics_16AndBelow() {
+        Utils.assumePlatformAtOrBelow(Build.VERSION_CODES.JELLY_BEAN)
+
+        activityRule.scenario.onActivity { activity ->
+            val calculator = WindowMetricsCalculator.getOrCreate()
+            val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+            val actual = calculator.computeCurrentWindowMetrics(activity as Context)
+
+            assertEquals(0, actual.bounds.left)
+            assertEquals(0, actual.bounds.top)
+            assertEquals(wm.defaultDisplay.width, actual.bounds.right)
+            assertEquals(wm.defaultDisplay.height, actual.bounds.bottom)
+        }
+    }
+
+    @Test
+    fun testCurrentWindowMetrics_context_matchesMaximumMetrics() {
+        activityRule.scenario.onActivity { activity ->
+            val calculator = WindowMetricsCalculator.getOrCreate()
+
+            val currentMetrics = calculator.computeCurrentWindowMetrics(activity as Context)
+            val maximumMetrics = calculator.computeMaximumWindowMetrics(activity as Context)
+
+            assertEquals(currentMetrics.bounds.left, maximumMetrics.bounds.left)
+            assertEquals(currentMetrics.bounds.top, maximumMetrics.bounds.top)
+            assertEquals(currentMetrics.bounds.right, maximumMetrics.bounds.right)
+            assertEquals(currentMetrics.bounds.bottom, maximumMetrics.bounds.bottom)
+        }
+    }
+
     /**
      * Tests that when applying a [Statement] then the decorator is removed. This is necessary to
      * keep tests hermetic. If this fails on the last test run then the fake implementation of
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityB.kt b/window/window-testing/src/androidTest/java/androidx/window/testing/layout/Utils.kt
similarity index 64%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityB.kt
copy to window/window-testing/src/androidTest/java/androidx/window/testing/layout/Utils.kt
index f47ab24..9c57994 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityB.kt
+++ b/window/window-testing/src/androidTest/java/androidx/window/testing/layout/Utils.kt
@@ -14,14 +14,17 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.testing.layout
 
-import android.graphics.Color
-import android.os.Bundle
+import android.os.Build
+import org.junit.Assume
 
-open class SplitPipActivityB : SplitPipActivityBase() {
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        viewBinding.rootSplitActivityLayout.setBackgroundColor(Color.parseColor("#fff3e0"))
+internal object Utils {
+    fun assumePlatformAtOrAbove(version: Int) {
+        Assume.assumeTrue(Build.VERSION.SDK_INT >= version)
+    }
+
+    fun assumePlatformAtOrBelow(version: Int) {
+        Assume.assumeTrue(Build.VERSION.SDK_INT <= version)
     }
 }
\ No newline at end of file
diff --git a/window/window-testing/src/main/java/androidx/window/testing/layout/StubMetricDecorator.kt b/window/window-testing/src/main/java/androidx/window/testing/layout/StubMetricDecorator.kt
index d02349a..14bf0c7 100644
--- a/window/window-testing/src/main/java/androidx/window/testing/layout/StubMetricDecorator.kt
+++ b/window/window-testing/src/main/java/androidx/window/testing/layout/StubMetricDecorator.kt
@@ -26,6 +26,6 @@
 @ExperimentalWindowApi
 internal object StubMetricDecorator : WindowMetricsCalculatorDecorator {
     override fun decorate(calculator: WindowMetricsCalculator): WindowMetricsCalculator {
-        return StubWindowMetricsCalculator
+        return StubWindowMetricsCalculator()
     }
 }
\ No newline at end of file
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 c9b994e..5a040ab 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
@@ -17,17 +17,25 @@
 package androidx.window.testing.layout
 
 import android.app.Activity
+import android.content.Context
+import android.graphics.Point
 import android.graphics.Rect
+import android.os.Build
+import android.view.Display
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
 import androidx.window.layout.WindowMetrics
 import androidx.window.layout.WindowMetricsCalculator
 
 /**
- * A stub implementation of [WindowMetricsCalculator] that returns the
- * [android.util.DisplayMetrics] for the current and maximum [WindowMetrics]. This is not correct
- * in general terms, as an application may be running in multi-window or otherwise adjusted to not
+ * A stub implementation of [WindowMetricsCalculator] that's intended to be used by Robolectric.
+ * [computeCurrentWindowMetrics] and [computeMaximumWindowMetrics] returns reasonable
+ * [WindowMetrics] for all supported SDK levels, but is not correct in general terms, as an
+ * application or [UiContext] may be running in multi-window mode, or otherwise adjusted to not
  * occupy the entire display.
  */
-internal object StubWindowMetricsCalculator : WindowMetricsCalculator {
+internal class StubWindowMetricsCalculator : WindowMetricsCalculator {
 
     override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
         val displayMetrics = activity.resources.displayMetrics
@@ -40,4 +48,52 @@
         val bounds = Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
         return WindowMetrics(bounds)
     }
+
+    // WindowManager#getDefaultDisplay is deprecated but we have this for compatibility with
+    // older versions.
+    @Suppress("DEPRECATION")
+    override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
+        val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            Api30Impl.getWindowMetrics(wm)
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            val displaySize = Point()
+            // We use getRealSize instead of getSize here because:
+            //   1) computeCurrentWindowMetrics and computeMaximumWindowMetrics in this class
+            //      always return a measurement equal to the entire display (see class-level
+            //      documentation).
+            //   2) getRealSize returns the largest region of the display, whereas getSize returns
+            //      the current app window. So to stay consistent with class documentation, we use
+            //      getRealSize.
+            Api17Impl.getRealSize(wm.defaultDisplay, displaySize)
+            val bounds = Rect(0, 0, displaySize.x, displaySize.y)
+            WindowMetrics(bounds)
+        } else {
+            val width = wm.defaultDisplay.width
+            val height = wm.defaultDisplay.height
+            val bounds = Rect(0, 0, width, height)
+            WindowMetrics(bounds)
+        }
+    }
+
+    override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
+        return computeCurrentWindowMetrics(context)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    private object Api30Impl {
+        fun getWindowMetrics(windowManager: WindowManager): WindowMetrics {
+            return WindowMetrics(windowManager.currentWindowMetrics.bounds)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+    private object Api17Impl {
+        // getRealSize is deprecated but we have this for compatibility with older versions.
+        @Suppress("DEPRECATION")
+        fun getRealSize(display: Display, point: Point) {
+            display.getRealSize(point)
+        }
+    }
 }
\ No newline at end of file
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index f0f8603..b664fd2 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -41,6 +41,7 @@
     ctor public ActivityRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters);
     method public androidx.window.embedding.ActivityRule build();
     method public androidx.window.embedding.ActivityRule.Builder setAlwaysExpand(boolean alwaysExpand);
+    method public androidx.window.embedding.ActivityRule.Builder setTag(String tag);
   }
 
   public final class ActivityStack {
@@ -51,6 +52,8 @@
   }
 
   public abstract class EmbeddingRule {
+    method public final String? getTag();
+    property public final String? tag;
   }
 
   public final class RuleController {
@@ -69,11 +72,95 @@
     method public java.util.Set<androidx.window.embedding.EmbeddingRule> parseRules(android.content.Context context, @XmlRes int staticRuleResourceId);
   }
 
+  public final class SplitAttributes {
+    method public int getAnimationBackgroundColor();
+    method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
+    method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+    property public final int animationBackgroundColor;
+    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;
+  }
+
+  public static final class SplitAttributes.Builder {
+    ctor public SplitAttributes.Builder();
+    method public androidx.window.embedding.SplitAttributes build();
+    method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int color);
+    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);
+  }
+
+  public static final class SplitAttributes.Companion {
+  }
+
+  public static final class SplitAttributes.LayoutDirection {
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection BOTTOM_TO_TOP;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection.Companion Companion;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection LEFT_TO_RIGHT;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection LOCALE;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection RIGHT_TO_LEFT;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection TOP_TO_BOTTOM;
+  }
+
+  public static final class SplitAttributes.LayoutDirection.Companion {
+  }
+
+  public static class SplitAttributes.SplitType {
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.ExpandContainersSplitType expandContainers();
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.HingeSplitType splitByHinge(optional androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType);
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+    field public static final androidx.window.embedding.SplitAttributes.SplitType.Companion Companion;
+  }
+
+  public static final class SplitAttributes.SplitType.Companion {
+    method public androidx.window.embedding.SplitAttributes.SplitType.ExpandContainersSplitType expandContainers();
+    method public androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
+    method public androidx.window.embedding.SplitAttributes.SplitType.HingeSplitType splitByHinge(optional androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType);
+    method public androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+  }
+
+  public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+  }
+
+  public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+    method public androidx.window.embedding.SplitAttributes.SplitType getFallbackSplitType();
+    property public final androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType;
+  }
+
+  public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+    method public float getRatio();
+    property public final float ratio;
+  }
+
+  public interface SplitAttributesCalculator {
+    method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
+  }
+
+  public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+    method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+    method public String? getSplitRuleTag();
+    method public boolean isDefaultMinSizeSatisfied();
+    property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+    property public final boolean isDefaultMinSizeSatisfied;
+    property public final android.content.res.Configuration parentConfiguration;
+    property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+    property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+    property public final String? splitRuleTag;
+  }
+
   public final class SplitController {
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+    method public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
+    method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
+    method public boolean isSplitAttributesCalculatorSupported();
     method public boolean isSplitSupported();
     method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+    method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
 
@@ -85,10 +172,10 @@
     method public operator boolean contains(android.app.Activity activity);
     method public androidx.window.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.embedding.ActivityStack getSecondaryActivityStack();
-    method public float getSplitRatio();
+    method public androidx.window.embedding.SplitAttributes getSplitAttributes();
     property public final androidx.window.embedding.ActivityStack primaryActivityStack;
     property public final androidx.window.embedding.ActivityStack secondaryActivityStack;
-    property public final float splitRatio;
+    property public final androidx.window.embedding.SplitAttributes splitAttributes;
   }
 
   public final class SplitPairFilter {
@@ -106,33 +193,34 @@
   public final class SplitPairRule extends androidx.window.embedding.SplitRule {
     method public boolean getClearTop();
     method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
-    method public int getFinishPrimaryWithSecondary();
-    method public int getFinishSecondaryWithPrimary();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithSecondary();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishSecondaryWithPrimary();
     property public final boolean clearTop;
     property public final java.util.Set<androidx.window.embedding.SplitPairFilter> filters;
-    property public final int finishPrimaryWithSecondary;
-    property public final int finishSecondaryWithPrimary;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithSecondary;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishSecondaryWithPrimary;
   }
 
   public static final class SplitPairRule.Builder {
     ctor public SplitPairRule.Builder(java.util.Set<androidx.window.embedding.SplitPairFilter> filters);
     method public androidx.window.embedding.SplitPairRule build();
     method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
-    method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
-    method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
-    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithSecondary);
+    method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(androidx.window.embedding.SplitRule.FinishBehavior finishSecondaryWithPrimary);
+    method public androidx.window.embedding.SplitPairRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
-    method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+    method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
   }
 
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
-    method public int getFinishPrimaryWithPlaceholder();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
     method public android.content.Intent getPlaceholderIntent();
     method public boolean isSticky();
     property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
-    property public final int finishPrimaryWithPlaceholder;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithPlaceholder;
     property public final boolean isSticky;
     property public final android.content.Intent placeholderIntent;
   }
@@ -140,33 +228,41 @@
   public static final class SplitPlaceholderRule.Builder {
     ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent);
     method public androidx.window.embedding.SplitPlaceholderRule build();
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithPlaceholder);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setTag(String? tag);
   }
 
   public class SplitRule extends androidx.window.embedding.EmbeddingRule {
-    method public final int getLayoutDirection();
+    method public final androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public final int getMinHeightDp();
     method public final int getMinSmallestWidthDp();
     method public final int getMinWidthDp();
-    method public final float getSplitRatio();
-    property public final int layoutDirection;
+    property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+    property public final int minHeightDp;
     property public final int minSmallestWidthDp;
     property public final int minWidthDp;
-    property public final float splitRatio;
     field public static final androidx.window.embedding.SplitRule.Companion Companion;
     field public static final int DEFAULT_SPLIT_MIN_DIMENSION_DP = 600; // 0x258
-    field public static final int FINISH_ADJACENT = 2; // 0x2
-    field public static final int FINISH_ALWAYS = 1; // 0x1
-    field public static final int FINISH_NEVER = 0; // 0x0
   }
 
   public static final class SplitRule.Companion {
   }
 
+  public static final class SplitRule.FinishBehavior {
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior ADJACENT;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior ALWAYS;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior.Companion Companion;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior NEVER;
+  }
+
+  public static final class SplitRule.FinishBehavior.Companion {
+  }
+
 }
 
 package androidx.window.layout {
@@ -216,6 +312,7 @@
 
   public interface WindowInfoTracker {
     method public default static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+    method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
     method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
     field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
   }
@@ -236,7 +333,9 @@
 
   public interface WindowMetricsCalculator {
     method public androidx.window.layout.WindowMetrics computeCurrentWindowMetrics(android.app.Activity activity);
+    method public default androidx.window.layout.WindowMetrics computeCurrentWindowMetrics(@UiContext android.content.Context context);
     method public androidx.window.layout.WindowMetrics computeMaximumWindowMetrics(android.app.Activity activity);
+    method public default androidx.window.layout.WindowMetrics computeMaximumWindowMetrics(@UiContext android.content.Context context);
     method public default static androidx.window.layout.WindowMetricsCalculator getOrCreate();
     field public static final androidx.window.layout.WindowMetricsCalculator.Companion Companion;
   }
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index f4e24a6..94e2e27 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -8,6 +8,38 @@
 
 }
 
+package androidx.window.area {
+
+  @androidx.window.core.ExperimentalWindowApi public interface WindowAreaController {
+    method public default static androidx.window.area.WindowAreaController getOrCreate();
+    field public static final androidx.window.area.WindowAreaController.Companion Companion;
+  }
+
+  public static final class WindowAreaController.Companion {
+    method public androidx.window.area.WindowAreaController getOrCreate();
+  }
+
+  @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSession {
+    method public void close();
+  }
+
+  @androidx.window.core.ExperimentalWindowApi public interface WindowAreaSessionCallback {
+    method public void onSessionEnded();
+    method public void onSessionStarted(androidx.window.area.WindowAreaSession session);
+  }
+
+  @androidx.window.core.ExperimentalWindowApi public final class WindowAreaStatus {
+    field public static final androidx.window.area.WindowAreaStatus AVAILABLE;
+    field public static final androidx.window.area.WindowAreaStatus.Companion Companion;
+    field public static final androidx.window.area.WindowAreaStatus UNAVAILABLE;
+    field public static final androidx.window.area.WindowAreaStatus UNSUPPORTED;
+  }
+
+  public static final class WindowAreaStatus.Companion {
+  }
+
+}
+
 package androidx.window.core {
 
   @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWindowApi {
@@ -48,6 +80,7 @@
     ctor public ActivityRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters);
     method public androidx.window.embedding.ActivityRule build();
     method public androidx.window.embedding.ActivityRule.Builder setAlwaysExpand(boolean alwaysExpand);
+    method public androidx.window.embedding.ActivityRule.Builder setTag(String tag);
   }
 
   public final class ActivityStack {
@@ -58,6 +91,8 @@
   }
 
   public abstract class EmbeddingRule {
+    method public final String? getTag();
+    property public final String? tag;
   }
 
   public final class RuleController {
@@ -76,11 +111,95 @@
     method public java.util.Set<androidx.window.embedding.EmbeddingRule> parseRules(android.content.Context context, @XmlRes int staticRuleResourceId);
   }
 
+  public final class SplitAttributes {
+    method public int getAnimationBackgroundColor();
+    method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
+    method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+    property public final int animationBackgroundColor;
+    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;
+  }
+
+  public static final class SplitAttributes.Builder {
+    ctor public SplitAttributes.Builder();
+    method public androidx.window.embedding.SplitAttributes build();
+    method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int color);
+    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);
+  }
+
+  public static final class SplitAttributes.Companion {
+  }
+
+  public static final class SplitAttributes.LayoutDirection {
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection BOTTOM_TO_TOP;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection.Companion Companion;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection LEFT_TO_RIGHT;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection LOCALE;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection RIGHT_TO_LEFT;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection TOP_TO_BOTTOM;
+  }
+
+  public static final class SplitAttributes.LayoutDirection.Companion {
+  }
+
+  public static class SplitAttributes.SplitType {
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.ExpandContainersSplitType expandContainers();
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.HingeSplitType splitByHinge(optional androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType);
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+    field public static final androidx.window.embedding.SplitAttributes.SplitType.Companion Companion;
+  }
+
+  public static final class SplitAttributes.SplitType.Companion {
+    method public androidx.window.embedding.SplitAttributes.SplitType.ExpandContainersSplitType expandContainers();
+    method public androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
+    method public androidx.window.embedding.SplitAttributes.SplitType.HingeSplitType splitByHinge(optional androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType);
+    method public androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+  }
+
+  public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+  }
+
+  public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+    method public androidx.window.embedding.SplitAttributes.SplitType getFallbackSplitType();
+    property public final androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType;
+  }
+
+  public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+    method public float getRatio();
+    property public final float ratio;
+  }
+
+  public interface SplitAttributesCalculator {
+    method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
+  }
+
+  public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+    method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+    method public String? getSplitRuleTag();
+    method public boolean isDefaultMinSizeSatisfied();
+    property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+    property public final boolean isDefaultMinSizeSatisfied;
+    property public final android.content.res.Configuration parentConfiguration;
+    property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+    property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+    property public final String? splitRuleTag;
+  }
+
   public final class SplitController {
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+    method public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
+    method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
+    method public boolean isSplitAttributesCalculatorSupported();
     method public boolean isSplitSupported();
     method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+    method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
 
@@ -92,10 +211,10 @@
     method public operator boolean contains(android.app.Activity activity);
     method public androidx.window.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.embedding.ActivityStack getSecondaryActivityStack();
-    method public float getSplitRatio();
+    method public androidx.window.embedding.SplitAttributes getSplitAttributes();
     property public final androidx.window.embedding.ActivityStack primaryActivityStack;
     property public final androidx.window.embedding.ActivityStack secondaryActivityStack;
-    property public final float splitRatio;
+    property public final androidx.window.embedding.SplitAttributes splitAttributes;
   }
 
   public final class SplitPairFilter {
@@ -113,33 +232,34 @@
   public final class SplitPairRule extends androidx.window.embedding.SplitRule {
     method public boolean getClearTop();
     method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
-    method public int getFinishPrimaryWithSecondary();
-    method public int getFinishSecondaryWithPrimary();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithSecondary();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishSecondaryWithPrimary();
     property public final boolean clearTop;
     property public final java.util.Set<androidx.window.embedding.SplitPairFilter> filters;
-    property public final int finishPrimaryWithSecondary;
-    property public final int finishSecondaryWithPrimary;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithSecondary;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishSecondaryWithPrimary;
   }
 
   public static final class SplitPairRule.Builder {
     ctor public SplitPairRule.Builder(java.util.Set<androidx.window.embedding.SplitPairFilter> filters);
     method public androidx.window.embedding.SplitPairRule build();
     method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
-    method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
-    method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
-    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithSecondary);
+    method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(androidx.window.embedding.SplitRule.FinishBehavior finishSecondaryWithPrimary);
+    method public androidx.window.embedding.SplitPairRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
-    method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+    method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
   }
 
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
-    method public int getFinishPrimaryWithPlaceholder();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
     method public android.content.Intent getPlaceholderIntent();
     method public boolean isSticky();
     property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
-    property public final int finishPrimaryWithPlaceholder;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithPlaceholder;
     property public final boolean isSticky;
     property public final android.content.Intent placeholderIntent;
   }
@@ -147,33 +267,41 @@
   public static final class SplitPlaceholderRule.Builder {
     ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent);
     method public androidx.window.embedding.SplitPlaceholderRule build();
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithPlaceholder);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setTag(String? tag);
   }
 
   public class SplitRule extends androidx.window.embedding.EmbeddingRule {
-    method public final int getLayoutDirection();
+    method public final androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public final int getMinHeightDp();
     method public final int getMinSmallestWidthDp();
     method public final int getMinWidthDp();
-    method public final float getSplitRatio();
-    property public final int layoutDirection;
+    property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+    property public final int minHeightDp;
     property public final int minSmallestWidthDp;
     property public final int minWidthDp;
-    property public final float splitRatio;
     field public static final androidx.window.embedding.SplitRule.Companion Companion;
     field public static final int DEFAULT_SPLIT_MIN_DIMENSION_DP = 600; // 0x258
-    field public static final int FINISH_ADJACENT = 2; // 0x2
-    field public static final int FINISH_ALWAYS = 1; // 0x1
-    field public static final int FINISH_NEVER = 0; // 0x0
   }
 
   public static final class SplitRule.Companion {
   }
 
+  public static final class SplitRule.FinishBehavior {
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior ADJACENT;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior ALWAYS;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior.Companion Companion;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior NEVER;
+  }
+
+  public static final class SplitRule.FinishBehavior.Companion {
+  }
+
 }
 
 package androidx.window.layout {
@@ -223,6 +351,7 @@
 
   public interface WindowInfoTracker {
     method public default static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+    method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
     method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
     field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
   }
@@ -244,7 +373,9 @@
 
   public interface WindowMetricsCalculator {
     method public androidx.window.layout.WindowMetrics computeCurrentWindowMetrics(android.app.Activity activity);
+    method public default androidx.window.layout.WindowMetrics computeCurrentWindowMetrics(@UiContext android.content.Context context);
     method public androidx.window.layout.WindowMetrics computeMaximumWindowMetrics(android.app.Activity activity);
+    method public default androidx.window.layout.WindowMetrics computeMaximumWindowMetrics(@UiContext android.content.Context context);
     method public default static androidx.window.layout.WindowMetricsCalculator getOrCreate();
     field public static final androidx.window.layout.WindowMetricsCalculator.Companion Companion;
   }
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index f0f8603..b664fd2 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -41,6 +41,7 @@
     ctor public ActivityRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters);
     method public androidx.window.embedding.ActivityRule build();
     method public androidx.window.embedding.ActivityRule.Builder setAlwaysExpand(boolean alwaysExpand);
+    method public androidx.window.embedding.ActivityRule.Builder setTag(String tag);
   }
 
   public final class ActivityStack {
@@ -51,6 +52,8 @@
   }
 
   public abstract class EmbeddingRule {
+    method public final String? getTag();
+    property public final String? tag;
   }
 
   public final class RuleController {
@@ -69,11 +72,95 @@
     method public java.util.Set<androidx.window.embedding.EmbeddingRule> parseRules(android.content.Context context, @XmlRes int staticRuleResourceId);
   }
 
+  public final class SplitAttributes {
+    method public int getAnimationBackgroundColor();
+    method public androidx.window.embedding.SplitAttributes.LayoutDirection getLayoutDirection();
+    method public androidx.window.embedding.SplitAttributes.SplitType getSplitType();
+    property public final int animationBackgroundColor;
+    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;
+  }
+
+  public static final class SplitAttributes.Builder {
+    ctor public SplitAttributes.Builder();
+    method public androidx.window.embedding.SplitAttributes build();
+    method public androidx.window.embedding.SplitAttributes.Builder setAnimationBackgroundColor(@ColorInt int color);
+    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);
+  }
+
+  public static final class SplitAttributes.Companion {
+  }
+
+  public static final class SplitAttributes.LayoutDirection {
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection BOTTOM_TO_TOP;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection.Companion Companion;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection LEFT_TO_RIGHT;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection LOCALE;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection RIGHT_TO_LEFT;
+    field public static final androidx.window.embedding.SplitAttributes.LayoutDirection TOP_TO_BOTTOM;
+  }
+
+  public static final class SplitAttributes.LayoutDirection.Companion {
+  }
+
+  public static class SplitAttributes.SplitType {
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.ExpandContainersSplitType expandContainers();
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.HingeSplitType splitByHinge(optional androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType);
+    method public static final androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+    field public static final androidx.window.embedding.SplitAttributes.SplitType.Companion Companion;
+  }
+
+  public static final class SplitAttributes.SplitType.Companion {
+    method public androidx.window.embedding.SplitAttributes.SplitType.ExpandContainersSplitType expandContainers();
+    method public androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType ratio(@FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float ratio);
+    method public androidx.window.embedding.SplitAttributes.SplitType.HingeSplitType splitByHinge(optional androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType);
+    method public androidx.window.embedding.SplitAttributes.SplitType.RatioSplitType splitEqually();
+  }
+
+  public static final class SplitAttributes.SplitType.ExpandContainersSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+  }
+
+  public static final class SplitAttributes.SplitType.HingeSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+    method public androidx.window.embedding.SplitAttributes.SplitType getFallbackSplitType();
+    property public final androidx.window.embedding.SplitAttributes.SplitType fallbackSplitType;
+  }
+
+  public static final class SplitAttributes.SplitType.RatioSplitType extends androidx.window.embedding.SplitAttributes.SplitType {
+    method public float getRatio();
+    property public final float ratio;
+  }
+
+  public interface SplitAttributesCalculator {
+    method public androidx.window.embedding.SplitAttributes computeSplitAttributesForParams(androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams params);
+  }
+
+  public static final class SplitAttributesCalculator.SplitAttributesCalculatorParams {
+    method public androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public android.content.res.Configuration getParentConfiguration();
+    method public androidx.window.layout.WindowLayoutInfo getParentWindowLayoutInfo();
+    method public androidx.window.layout.WindowMetrics getParentWindowMetrics();
+    method public String? getSplitRuleTag();
+    method public boolean isDefaultMinSizeSatisfied();
+    property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+    property public final boolean isDefaultMinSizeSatisfied;
+    property public final android.content.res.Configuration parentConfiguration;
+    property public final androidx.window.layout.WindowLayoutInfo parentWindowLayoutInfo;
+    property public final androidx.window.layout.WindowMetrics parentWindowMetrics;
+    property public final String? splitRuleTag;
+  }
+
   public final class SplitController {
     method public void addSplitListener(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+    method public void clearSplitAttributesCalculator();
     method public static androidx.window.embedding.SplitController getInstance(android.content.Context context);
+    method public androidx.window.embedding.SplitAttributesCalculator? getSplitAttributesCalculator();
+    method public boolean isSplitAttributesCalculatorSupported();
     method public boolean isSplitSupported();
     method public void removeSplitListener(androidx.core.util.Consumer<java.util.List<androidx.window.embedding.SplitInfo>> consumer);
+    method public void setSplitAttributesCalculator(androidx.window.embedding.SplitAttributesCalculator calculator);
     field public static final androidx.window.embedding.SplitController.Companion Companion;
   }
 
@@ -85,10 +172,10 @@
     method public operator boolean contains(android.app.Activity activity);
     method public androidx.window.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.embedding.ActivityStack getSecondaryActivityStack();
-    method public float getSplitRatio();
+    method public androidx.window.embedding.SplitAttributes getSplitAttributes();
     property public final androidx.window.embedding.ActivityStack primaryActivityStack;
     property public final androidx.window.embedding.ActivityStack secondaryActivityStack;
-    property public final float splitRatio;
+    property public final androidx.window.embedding.SplitAttributes splitAttributes;
   }
 
   public final class SplitPairFilter {
@@ -106,33 +193,34 @@
   public final class SplitPairRule extends androidx.window.embedding.SplitRule {
     method public boolean getClearTop();
     method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
-    method public int getFinishPrimaryWithSecondary();
-    method public int getFinishSecondaryWithPrimary();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithSecondary();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishSecondaryWithPrimary();
     property public final boolean clearTop;
     property public final java.util.Set<androidx.window.embedding.SplitPairFilter> filters;
-    property public final int finishPrimaryWithSecondary;
-    property public final int finishSecondaryWithPrimary;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithSecondary;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishSecondaryWithPrimary;
   }
 
   public static final class SplitPairRule.Builder {
     ctor public SplitPairRule.Builder(java.util.Set<androidx.window.embedding.SplitPairFilter> filters);
     method public androidx.window.embedding.SplitPairRule build();
     method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
-    method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
-    method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
-    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPairRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithSecondary);
+    method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(androidx.window.embedding.SplitRule.FinishBehavior finishSecondaryWithPrimary);
+    method public androidx.window.embedding.SplitPairRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
-    method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
+    method public androidx.window.embedding.SplitPairRule.Builder setTag(String? tag);
   }
 
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
-    method public int getFinishPrimaryWithPlaceholder();
+    method public androidx.window.embedding.SplitRule.FinishBehavior getFinishPrimaryWithPlaceholder();
     method public android.content.Intent getPlaceholderIntent();
     method public boolean isSticky();
     property public final java.util.Set<androidx.window.embedding.ActivityFilter> filters;
-    property public final int finishPrimaryWithPlaceholder;
+    property public final androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithPlaceholder;
     property public final boolean isSticky;
     property public final android.content.Intent placeholderIntent;
   }
@@ -140,33 +228,41 @@
   public static final class SplitPlaceholderRule.Builder {
     ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent);
     method public androidx.window.embedding.SplitPlaceholderRule build();
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setDefaultSplitAttributes(androidx.window.embedding.SplitAttributes defaultSplitAttributes);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(androidx.window.embedding.SplitRule.FinishBehavior finishPrimaryWithPlaceholder);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinHeightDp(@IntRange(from=0L) int minHeightDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setTag(String? tag);
   }
 
   public class SplitRule extends androidx.window.embedding.EmbeddingRule {
-    method public final int getLayoutDirection();
+    method public final androidx.window.embedding.SplitAttributes getDefaultSplitAttributes();
+    method public final int getMinHeightDp();
     method public final int getMinSmallestWidthDp();
     method public final int getMinWidthDp();
-    method public final float getSplitRatio();
-    property public final int layoutDirection;
+    property public final androidx.window.embedding.SplitAttributes defaultSplitAttributes;
+    property public final int minHeightDp;
     property public final int minSmallestWidthDp;
     property public final int minWidthDp;
-    property public final float splitRatio;
     field public static final androidx.window.embedding.SplitRule.Companion Companion;
     field public static final int DEFAULT_SPLIT_MIN_DIMENSION_DP = 600; // 0x258
-    field public static final int FINISH_ADJACENT = 2; // 0x2
-    field public static final int FINISH_ALWAYS = 1; // 0x1
-    field public static final int FINISH_NEVER = 0; // 0x0
   }
 
   public static final class SplitRule.Companion {
   }
 
+  public static final class SplitRule.FinishBehavior {
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior ADJACENT;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior ALWAYS;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior.Companion Companion;
+    field public static final androidx.window.embedding.SplitRule.FinishBehavior NEVER;
+  }
+
+  public static final class SplitRule.FinishBehavior.Companion {
+  }
+
 }
 
 package androidx.window.layout {
@@ -216,6 +312,7 @@
 
   public interface WindowInfoTracker {
     method public default static androidx.window.layout.WindowInfoTracker getOrCreate(android.content.Context context);
+    method public default kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(@UiContext android.content.Context context);
     method public kotlinx.coroutines.flow.Flow<androidx.window.layout.WindowLayoutInfo> windowLayoutInfo(android.app.Activity activity);
     field public static final androidx.window.layout.WindowInfoTracker.Companion Companion;
   }
@@ -236,7 +333,9 @@
 
   public interface WindowMetricsCalculator {
     method public androidx.window.layout.WindowMetrics computeCurrentWindowMetrics(android.app.Activity activity);
+    method public default androidx.window.layout.WindowMetrics computeCurrentWindowMetrics(@UiContext android.content.Context context);
     method public androidx.window.layout.WindowMetrics computeMaximumWindowMetrics(android.app.Activity activity);
+    method public default androidx.window.layout.WindowMetrics computeMaximumWindowMetrics(@UiContext android.content.Context context);
     method public default static androidx.window.layout.WindowMetricsCalculator getOrCreate();
     field public static final androidx.window.layout.WindowMetricsCalculator.Companion Companion;
   }
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 5236d42..d671511 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -45,12 +45,12 @@
 dependencies {
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesAndroid)
-    implementation("androidx.annotation:annotation:1.2.0")
+    implementation("androidx.annotation:annotation:1.3.0")
     implementation("androidx.collection:collection:1.1.0")
     implementation("androidx.core:core:1.8.0")
 
     compileOnly(project(":window:sidecar:sidecar"))
-    compileOnly("androidx.window.extensions:extensions:1.1.0-alpha02")
+    compileOnly(project(":window:extensions:extensions"))
 
     testImplementation(libs.testCore)
     testImplementation(libs.testRunner)
@@ -61,7 +61,7 @@
     testImplementation(libs.mockitoKotlin4)
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation(compileOnly(project(":window:sidecar:sidecar")))
-    testImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-alpha02"))
+    testImplementation(compileOnly(project(":window:extensions:extensions")))
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.kotlinTestJunit)
@@ -75,9 +75,8 @@
     androidTestImplementation(libs.multidex)
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
+    androidTestImplementation(compileOnly(project(":window:extensions:extensions")))
     androidTestImplementation(compileOnly(project(":window:sidecar:sidecar")))
-    androidTestImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-alpha02"))
-    samples(project(":window:window-samples"))
 }
 
 androidx {
diff --git a/window/window/samples/build.gradle b/window/window/samples/build.gradle
new file mode 100644
index 0000000..a391e0a
--- /dev/null
+++ b/window/window/samples/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api(project(":window:window"))
+
+    compileOnly(project(":annotation:annotation-sampled"))
+}
+
+android {
+    namespace "androidx.window.samples"
+}
+
+androidx {
+    name = "Jetpack WindowManager Library Samples"
+    type = LibraryType.SAMPLES
+    inceptionYear = "2022"
+    description = "Code samples for WindowManager Jetpack library."
+}
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
new file mode 100644
index 0000000..b5e4534
--- /dev/null
+++ b/window/window/samples/src/main/java/androidx.window.samples.embedding/SplitAttributesCalculatorSamples.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.window.samples.embedding
+
+import android.app.Application
+import android.graphics.Color
+import androidx.annotation.Sampled
+import androidx.window.embedding.SplitAttributes
+import androidx.window.embedding.SplitAttributesCalculator
+import androidx.window.embedding.SplitController
+import androidx.window.layout.FoldingFeature
+
+@Sampled
+fun splitAttributesCalculatorSample() {
+    SplitController.getInstance(context)
+        .setSplitAttributesCalculator(
+            object : SplitAttributesCalculator {
+        override fun computeSplitAttributesForParams(
+            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
+        ): SplitAttributes {
+            val tag = params.splitRuleTag
+            val parentWindowMetrics = params.parentWindowMetrics
+            val parentConfig = params.parentConfiguration
+            val foldingFeatures = params.parentWindowLayoutInfo.displayFeatures
+                .filterIsInstance<FoldingFeature>()
+            val foldingState = if (foldingFeatures.size == 1) foldingFeatures[0] else null
+            // Tag can be used to filter the SplitRule to apply the SplitAttributes
+            if (TAG_SPLIT_RULE_MAIN != tag && params.isDefaultMinSizeSatisfied) {
+                return params.defaultSplitAttributes
+            }
+
+            // This sample will make the app show a layout to
+            // - split the task bounds vertically if the device is in landscape
+            // - fill the task bounds if the device is in portrait and its folding state does not
+            //   split the screen
+            // - split the task bounds horizontally in tabletop mode
+            val bounds = parentWindowMetrics.bounds
+            if (foldingState?.isSeparating == true) {
+                // Split the parent container that followed by the hinge if the hinge separates the
+                // parent window.
+                return SplitAttributes.Builder()
+                    .setSplitType(SplitAttributes.SplitType.splitByHinge())
+                    .setLayoutDirection(
+                        if (foldingState.orientation == FoldingFeature.Orientation.HORIZONTAL) {
+                            SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+                        } else {
+                            SplitAttributes.LayoutDirection.LOCALE
+                        }
+                    )
+                    // Set the color to use when switching between vertical and horizontal
+                    .setAnimationBackgroundColor(Color.GRAY)
+                    .build()
+            }
+            return if (parentConfig.screenWidthDp >= 600 && bounds.width() >= bounds.height()) {
+                // Split the parent container equally and vertically if the device is in landscape.
+                SplitAttributes.Builder()
+                    .setSplitType(SplitAttributes.SplitType.splitEqually())
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                    .setAnimationBackgroundColor(Color.GRAY)
+                    .build()
+            } else {
+                // Expand containers if the device is in portrait or the width is less than 600 dp.
+                SplitAttributes.Builder()
+                    .setSplitType(SplitAttributes.SplitType.expandContainers())
+                    .build()
+            }
+        }
+    })
+}
+
+@Sampled
+fun splitWithOrientations() {
+    SplitController.getInstance(context)
+        .setSplitAttributesCalculator(
+            object : SplitAttributesCalculator {
+        override fun computeSplitAttributesForParams(
+            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
+        ): SplitAttributes {
+            // A sample to split with the dimension that larger than 600 DP. If there's no dimension
+            // larger than 600 DP, show the presentation to fill the task bounds.
+            val parentConfiguration = params.parentConfiguration
+            val builder = SplitAttributes.Builder()
+            return if (parentConfiguration.screenWidthDp >= 600) {
+                builder
+                    .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                    // Set the color to use when switching between vertical and horizontal
+                    .setAnimationBackgroundColor(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
+                    .setAnimationBackgroundColor(Color.GRAY)
+                    .build()
+            } else {
+                // Fallback to expand the secondary container
+                builder
+                    .setSplitType(SplitAttributes.SplitType.expandContainers())
+                    .build()
+            }
+        }
+    })
+}
+
+@Sampled
+fun expandContainersInPortrait() {
+    SplitController.getInstance(context)
+        .setSplitAttributesCalculator(
+            object : SplitAttributesCalculator {
+        override fun computeSplitAttributesForParams(
+            params: SplitAttributesCalculator.SplitAttributesCalculatorParams
+        ): SplitAttributes {
+            // A sample to always fill task bounds when the device is in portrait.
+            val tag = params.splitRuleTag
+            val bounds = params.parentWindowMetrics.bounds
+            val defaultSplitAttributes = params.defaultSplitAttributes
+            val isDefaultMinSizeSatisfied = params.isDefaultMinSizeSatisfied
+
+            val expandContainersAttrs = SplitAttributes.Builder()
+                .setSplitType(SplitAttributes.SplitType.expandContainers())
+                .build()
+            if (!isDefaultMinSizeSatisfied) {
+                return expandContainersAttrs
+            }
+            // Always expand containers for the splitRule tagged as
+            // TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT if the device is in portrait
+            // even if [isDefaultMinSizeSatisfied] reports true.
+            if (bounds.height() > bounds.width() && TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT.equals(tag)) {
+                return expandContainersAttrs
+            }
+            // Otherwise, use the default splitAttributes.
+            return defaultSplitAttributes
+        }
+    })
+}
+
+/** Assume it's a valid [Application]... */
+val context = Application()
+const val TAG_SPLIT_RULE_MAIN = "main"
+const val TAG_SPLIT_RULE_EXPAND_IN_PORTRAIT = "expand_in_portrait"
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
new file mode 100644
index 0000000..d9ae80b
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/WindowTestUtils.kt
@@ -0,0 +1,42 @@
+package androidx.window
+
+import android.app.Application
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.view.Display
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.test.core.app.ApplicationProvider
+import androidx.window.core.ExtensionsUtil
+import org.junit.Assume.assumeTrue
+
+open class WindowTestUtils {
+    companion object {
+
+        @RequiresApi(Build.VERSION_CODES.R)
+        fun createOverlayWindowContext(): Context {
+            val context = ApplicationProvider.getApplicationContext<Application>()
+            return context.createDisplayContext(
+                context.getSystemService(DisplayManager::class.java)
+                    .getDisplay(Display.DEFAULT_DISPLAY)
+            ).createWindowContext(
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+                /* options= */ null
+            )
+        }
+
+        @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+        fun assumeAtLeastVendorApiLevel(min: Int) {
+            val apiLevel = ExtensionsUtil.safeVendorApiLevel
+            assumeTrue(apiLevel >= min)
+        }
+
+        @OptIn(androidx.window.core.ExperimentalWindowApi::class)
+        fun assumeBeforeVendorApiLevel(max: Int) {
+            val apiLevel = ExtensionsUtil.safeVendorApiLevel
+            assumeTrue(apiLevel < max)
+            assumeTrue(apiLevel > 0)
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
new file mode 100644
index 0000000..0432ad5
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 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.window.area
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.window.TestActivity
+import androidx.window.TestConsumer
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import java.util.function.Consumer
+import kotlin.test.assertFailsWith
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalWindowApi::class)
+class WindowAreaControllerImplTest {
+
+    @get:Rule
+    public val activityScenario: ActivityScenarioRule<TestActivity> =
+        ActivityScenarioRule(TestActivity::class.java)
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayStatus(): Unit = testScope.runTest {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.N)
+        activityScenario.scenario.onActivity {
+            val extensionComponent = FakeWindowAreaComponent()
+            val repo = WindowAreaControllerImpl(extensionComponent)
+            val collector = TestConsumer<WindowAreaStatus>()
+            extensionComponent
+                .updateStatusListeners(WindowAreaComponent.STATUS_UNAVAILABLE)
+            testScope.launch(Job()) {
+                repo.rearDisplayStatus().collect(collector::accept)
+            }
+            collector.assertValue(WindowAreaStatus.UNAVAILABLE)
+            extensionComponent
+                .updateStatusListeners(WindowAreaComponent.STATUS_AVAILABLE)
+            collector.assertValues(
+                WindowAreaStatus.UNAVAILABLE,
+                WindowAreaStatus.AVAILABLE
+            )
+        }
+    }
+
+    @Test
+    public fun testRearDisplayStatusNullComponent(): Unit = testScope.runTest {
+        activityScenario.scenario.onActivity {
+            val repo = EmptyWindowAreaControllerImpl()
+            val collector = TestConsumer<WindowAreaStatus>()
+            testScope.launch(Job()) {
+                repo.rearDisplayStatus().collect(collector::accept)
+            }
+            collector.assertValue(WindowAreaStatus.UNSUPPORTED)
+        }
+    }
+
+    /**
+     * Tests the rear display mode flow works as expected. Tests the flow
+     * through WindowAreaControllerImpl with a fake extension. This fake extension
+     * changes the orientation of the activity to landscape when rear display mode is enabled
+     * and then returns it back to portrait when it's disabled.
+     */
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayMode(): Unit = testScope.runTest {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.N)
+        val extensions = FakeWindowAreaComponent()
+        val repo = WindowAreaControllerImpl(extensions)
+        extensions.currentStatus = WindowAreaComponent.STATUS_AVAILABLE
+        val callback = TestWindowAreaSessionCallback()
+        activityScenario.scenario.onActivity { testActivity ->
+            testActivity.resetLayoutCounter()
+            testActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+            testActivity.waitForLayout()
+        }
+
+        activityScenario.scenario.onActivity { testActivity ->
+            assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+            testActivity.resetLayoutCounter()
+            repo.rearDisplayMode(testActivity, Runnable::run, callback)
+        }
+
+        activityScenario.scenario.onActivity { testActivity ->
+            assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+            assert(callback.currentSession != null)
+            testActivity.resetLayoutCounter()
+            callback.endSession()
+        }
+        activityScenario.scenario.onActivity { testActivity ->
+            assert(testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+            assert(callback.currentSession == null)
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayModeReturnsError(): Unit = testScope.runTest {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.N)
+        val extensionComponent = FakeWindowAreaComponent()
+        extensionComponent.currentStatus = WindowAreaComponent.STATUS_UNAVAILABLE
+        val repo = WindowAreaControllerImpl(extensionComponent)
+        val callback = TestWindowAreaSessionCallback()
+        activityScenario.scenario.onActivity { testActivity ->
+            assertFailsWith(
+                exceptionClass = UnsupportedOperationException::class,
+                block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+            )
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    @Test
+    public fun testRearDisplayModeNullComponent(): Unit = testScope.runTest {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.N)
+        val repo = EmptyWindowAreaControllerImpl()
+        val callback = TestWindowAreaSessionCallback()
+        activityScenario.scenario.onActivity { testActivity ->
+            assertFailsWith(
+                exceptionClass = UnsupportedOperationException::class,
+                block = { repo.rearDisplayMode(testActivity, Runnable::run, callback) }
+            )
+        }
+    }
+
+    private class FakeWindowAreaComponent : WindowAreaComponent {
+        val statusListeners = mutableListOf<Consumer<Int>>()
+        var currentStatus = WindowAreaComponent.STATUS_UNSUPPORTED
+        var testActivity: Activity? = null
+        var sessionConsumer: Consumer<Int>? = null
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+            statusListeners.add(consumer)
+            consumer.accept(currentStatus)
+        }
+
+        override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+            statusListeners.remove(consumer)
+        }
+
+        // Fake WindowAreaComponent will change the orientation of the activity to signal
+        // entering rear display mode, as well as ending the session
+        @RequiresApi(Build.VERSION_CODES.N)
+        override fun startRearDisplaySession(
+            activity: Activity,
+            rearDisplaySessionConsumer: Consumer<Int>
+        ) {
+            if (currentStatus != WindowAreaComponent.STATUS_AVAILABLE) {
+                throw WindowAreaController.REAR_DISPLAY_ERROR
+            }
+            testActivity = activity
+            sessionConsumer = rearDisplaySessionConsumer
+            testActivity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+            rearDisplaySessionConsumer.accept(WindowAreaComponent.SESSION_STATE_ACTIVE)
+        }
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        override fun endRearDisplaySession() {
+            testActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+            sessionConsumer?.accept(WindowAreaComponent.SESSION_STATE_INACTIVE)
+        }
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        fun updateStatusListeners(newStatus: Int) {
+            currentStatus = newStatus
+            for (consumer in statusListeners) {
+                consumer.accept(currentStatus)
+            }
+        }
+    }
+
+    private class TestWindowAreaSessionCallback : WindowAreaSessionCallback {
+
+        var currentSession: WindowAreaSession? = null
+        var error: Throwable? = null
+
+        override fun onSessionStarted(session: WindowAreaSession) {
+            currentSession = session
+        }
+
+        override fun onSessionEnded() {
+            currentSession = null
+        }
+
+        fun endSession() = currentSession?.close()
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
new file mode 100644
index 0000000..be61996
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingAdapterTest.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.window.embedding
+
+import androidx.window.extensions.embedding.ActivityStack as OEMActivityStack
+import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
+import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
+import android.app.Activity
+import android.graphics.Color
+import androidx.window.WindowTestUtils
+import androidx.window.core.PredicateAdapter
+import androidx.window.embedding.SplitAttributes.SplitType
+import androidx.window.extensions.WindowExtensions
+import com.nhaarman.mockitokotlin2.doReturn
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+
+/** Tests for [EmbeddingAdapter] */
+class EmbeddingAdapterTest {
+    private lateinit var adapter: EmbeddingAdapter
+
+    @Before
+    fun setUp() {
+        adapter = EmbeddingBackend::class.java.classLoader?.let { loader ->
+            EmbeddingAdapter(PredicateAdapter(loader))
+        }!!
+    }
+
+    @Test
+    fun testTranslateSplitInfoWithDefaultAttrs() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_2)
+
+        val oemSplitInfo = OEMSplitInfo(
+            OEMActivityStack(ArrayList(), true),
+            OEMActivityStack(ArrayList(), true),
+            OEMSplitAttributes.Builder().build(),
+        )
+        val expectedSplitInfo = SplitInfo(
+            ActivityStack(ArrayList(), isEmpty = true),
+            ActivityStack(ArrayList(), isEmpty = true),
+            SplitAttributes.Builder()
+                .setSplitType(SplitType.splitEqually())
+                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                .setAnimationBackgroundColor(0)
+                .build()
+        )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Test
+    fun testTranslateSplitInfoWithExpandingContainers() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_2)
+
+        val oemSplitInfo = OEMSplitInfo(
+            OEMActivityStack(ArrayList(), true),
+            OEMActivityStack(ArrayList(), true),
+            OEMSplitAttributes.Builder()
+                .setSplitType(OEMSplitAttributes.SplitType.ExpandContainersSplitType())
+                .build(),
+        )
+        val expectedSplitInfo = SplitInfo(
+            ActivityStack(ArrayList(), isEmpty = true),
+            ActivityStack(ArrayList(), isEmpty = true),
+            SplitAttributes.Builder()
+                .setSplitType(SplitType.expandContainers())
+                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                .build()
+        )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun testTranslateSplitInfoWithApiLevel1() {
+        WindowTestUtils.assumeBeforeVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_2)
+
+        val activityStack = OEMActivityStack(ArrayList<Activity>(), true)
+        val expectedSplitRatio = 0.3f
+        val oemSplitInfo = mock(OEMSplitInfo::class.java)
+        doReturn(activityStack).`when`(oemSplitInfo).primaryActivityStack
+        doReturn(activityStack).`when`(oemSplitInfo).secondaryActivityStack
+        doReturn(expectedSplitRatio).`when`(oemSplitInfo).splitRatio
+
+        val expectedSplitInfo = SplitInfo(
+            ActivityStack(ArrayList(), isEmpty = true),
+            ActivityStack(ArrayList(), isEmpty = true),
+            SplitAttributes.Builder()
+                .setSplitType(SplitType.ratio(expectedSplitRatio))
+                // OEMSplitInfo with Vendor API level 1 doesn't provide layoutDirection.
+                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+                .build()
+        )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+
+    @Test
+    fun testTranslateSplitInfoWithApiLevel2() {
+        WindowTestUtils.assumeAtLeastVendorApiLevel(WindowExtensions.VENDOR_API_LEVEL_2)
+
+        val oemSplitInfo = OEMSplitInfo(
+            OEMActivityStack(ArrayList<Activity>(), true),
+            OEMActivityStack(ArrayList<Activity>(), true),
+            OEMSplitAttributes.Builder()
+                .setSplitType(
+                    OEMSplitAttributes.SplitType.HingeSplitType(
+                        OEMSplitAttributes.SplitType.RatioSplitType(0.3f)
+                    )
+                ).setLayoutDirection(OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+                .setAnimationBackgroundColor(Color.YELLOW)
+                .build(),
+        )
+        val expectedSplitInfo = SplitInfo(
+            ActivityStack(ArrayList(), isEmpty = true),
+            ActivityStack(ArrayList(), isEmpty = true),
+            SplitAttributes.Builder()
+                .setSplitType(SplitType.splitByHinge(SplitType.ratio(0.3f)))
+                .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
+                .setAnimationBackgroundColor(Color.YELLOW)
+                .build()
+        )
+        assertEquals(listOf(expectedSplitInfo), adapter.translate(listOf(oemSplitInfo)))
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
index cb44e6b..362e500 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
@@ -19,18 +19,26 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Color
 import android.graphics.Rect
-import android.util.LayoutDirection
+import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.test.core.app.ApplicationProvider
+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
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
 import androidx.window.embedding.SplitRule.Companion.DEFAULT_SPLIT_MIN_DIMENSION_DP
-import androidx.window.embedding.SplitRule.Companion.FINISH_ADJACENT
-import androidx.window.embedding.SplitRule.Companion.FINISH_ALWAYS
-import androidx.window.embedding.SplitRule.Companion.FINISH_NEVER
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ADJACENT
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
 import androidx.window.test.R
+import junit.framework.TestCase.assertNull
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertThrows
 import org.junit.Assert.assertTrue
+import org.junit.Before
 import org.junit.Test
 
 /**
@@ -41,7 +49,17 @@
  */
 class EmbeddingRuleConstructionTests {
     private val application = ApplicationProvider.getApplicationContext<Context>()
+    private val ruleController = RuleController.getInstance(application)
     private val density = application.resources.displayMetrics.density
+    private lateinit var validBounds: Rect
+    private lateinit var invalidBounds: Rect
+
+    @Before
+    fun setUp() {
+        validBounds = minValidWindowBounds()
+        invalidBounds = almostValidWindowBounds()
+        ruleController.clearRules()
+    }
 
     /**
      * Verifies that default params are set correctly when reading {@link SplitPairRule} from XML.
@@ -52,13 +70,39 @@
             .parseRules(application, R.xml.test_split_config_default_split_pair_rule)
         assertEquals(1, rules.size)
         val rule: SplitPairRule = rules.first() as SplitPairRule
-        assertEquals(FINISH_NEVER, rule.finishPrimaryWithSecondary)
-        assertEquals(FINISH_ALWAYS, rule.finishSecondaryWithPrimary)
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
+            .setLayoutDirection(LOCALE)
+            .setAnimationBackgroundColor(0)
+            .build()
+        assertNull(rule.tag)
+        assertEquals(NEVER, rule.finishPrimaryWithSecondary)
+        assertEquals(ALWAYS, rule.finishSecondaryWithPrimary)
         assertEquals(false, rule.clearTop)
-        assertEquals(0.5f, rule.splitRatio)
-        assertEquals(LayoutDirection.LOCALE, rule.layoutDirection)
-        assertTrue(rule.checkParentBounds(density, minValidWindowBounds()))
-        assertFalse(rule.checkParentBounds(density, almostValidWindowBounds()))
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertTrue(rule.checkParentBounds(density, validBounds))
+        assertFalse(rule.checkParentBounds(density, invalidBounds))
+    }
+
+    /** Verifies that horizontal layout are set correctly when reading [SplitPairRule] from XML. */
+    @Test
+    fun testHorizontalLayout_SplitPairRule_Xml() {
+        val rules = RuleController
+            .parseRules(application, R.xml.test_split_config_split_pair_rule_horizontal_layout)
+        assertEquals(1, rules.size)
+        val rule: SplitPairRule = rules.first() as SplitPairRule
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
+            .setLayoutDirection(TOP_TO_BOTTOM)
+            .setAnimationBackgroundColor(Color.BLUE)
+            .build()
+        assertEquals(TEST_TAG, rule.tag)
+        assertEquals(NEVER, rule.finishPrimaryWithSecondary)
+        assertEquals(ALWAYS, rule.finishSecondaryWithPrimary)
+        assertEquals(false, rule.clearTop)
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertTrue(rule.checkParentBounds(density, validBounds))
+        assertFalse(rule.checkParentBounds(density, invalidBounds))
     }
 
     /**
@@ -68,13 +112,18 @@
     @Test
     fun testDefaults_SplitPairRule_Builder() {
         val rule = SplitPairRule.Builder(HashSet()).build()
-        assertEquals(FINISH_NEVER, rule.finishPrimaryWithSecondary)
-        assertEquals(FINISH_ALWAYS, rule.finishSecondaryWithPrimary)
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
+            .setLayoutDirection(LOCALE)
+            .setAnimationBackgroundColor(0)
+            .build()
+        assertNull(rule.tag)
+        assertEquals(NEVER, rule.finishPrimaryWithSecondary)
+        assertEquals(ALWAYS, rule.finishSecondaryWithPrimary)
         assertEquals(false, rule.clearTop)
-        assertEquals(0.5f, rule.splitRatio)
-        assertEquals(LayoutDirection.LOCALE, rule.layoutDirection)
-        assertTrue(rule.checkParentBounds(density, minValidWindowBounds()))
-        assertFalse(rule.checkParentBounds(density, almostValidWindowBounds()))
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertTrue(rule.checkParentBounds(density, validBounds))
+        assertFalse(rule.checkParentBounds(density, invalidBounds))
     }
 
     /**
@@ -84,6 +133,11 @@
     @Test
     fun test_SplitPairRule_Builder() {
         val filters = HashSet<SplitPairFilter>()
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
+            .setLayoutDirection(LEFT_TO_RIGHT)
+            .setAnimationBackgroundColor(Color.GREEN)
+            .build()
         filters.add(
             SplitPairFilter(
                 ComponentName("a", "b"),
@@ -93,21 +147,23 @@
         )
         val rule = SplitPairRule.Builder(filters)
             .setMinWidthDp(123)
-            .setMinSmallestWidthDp(456)
-            .setFinishPrimaryWithSecondary(FINISH_ADJACENT)
-            .setFinishSecondaryWithPrimary(FINISH_ADJACENT)
+            .setMinHeightDp(456)
+            .setMinSmallestWidthDp(789)
+            .setFinishPrimaryWithSecondary(ADJACENT)
+            .setFinishSecondaryWithPrimary(ADJACENT)
             .setClearTop(true)
-            .setSplitRatio(0.3f)
-            .setLayoutDirection(LayoutDirection.LTR)
+            .setDefaultSplitAttributes(expectedSplitLayout)
+            .setTag(TEST_TAG)
             .build()
-        assertEquals(FINISH_ADJACENT, rule.finishPrimaryWithSecondary)
-        assertEquals(FINISH_ADJACENT, rule.finishSecondaryWithPrimary)
+        assertEquals(ADJACENT, rule.finishPrimaryWithSecondary)
+        assertEquals(ADJACENT, rule.finishSecondaryWithPrimary)
         assertEquals(true, rule.clearTop)
-        assertEquals(0.3f, rule.splitRatio)
-        assertEquals(LayoutDirection.LTR, rule.layoutDirection)
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertEquals(TEST_TAG, rule.tag)
         assertEquals(filters, rule.filters)
         assertEquals(123, rule.minWidthDp)
-        assertEquals(456, rule.minSmallestWidthDp)
+        assertEquals(456, rule.minHeightDp)
+        assertEquals(789, rule.minSmallestWidthDp)
     }
 
     /**
@@ -119,29 +175,24 @@
         assertThrows(IllegalArgumentException::class.java) {
             SplitPairRule.Builder(HashSet())
                 .setMinWidthDp(-1)
-                .setMinSmallestWidthDp(456)
+                .setMinHeightDp(456)
+                .setMinSmallestWidthDp(789)
                 .build()
         }
         assertThrows(IllegalArgumentException::class.java) {
             SplitPairRule.Builder(HashSet())
                 .setMinWidthDp(123)
+                .setMinHeightDp(-1)
+                .setMinSmallestWidthDp(789)
+                .build()
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitPairRule.Builder(HashSet())
+                .setMinWidthDp(123)
+                .setMinHeightDp(456)
                 .setMinSmallestWidthDp(-1)
                 .build()
         }
-        assertThrows(IllegalArgumentException::class.java) {
-            SplitPairRule.Builder(HashSet())
-                .setMinWidthDp(123)
-                .setMinSmallestWidthDp(456)
-                .setSplitRatio(-1.0f)
-                .build()
-        }
-        assertThrows(IllegalArgumentException::class.java) {
-            SplitPairRule.Builder(HashSet())
-                .setMinWidthDp(123)
-                .setMinSmallestWidthDp(456)
-                .setSplitRatio(1.1f)
-                .build()
-        }
     }
 
     /**
@@ -154,12 +205,41 @@
             .parseRules(application, R.xml.test_split_config_default_split_placeholder_rule)
         assertEquals(1, rules.size)
         val rule: SplitPlaceholderRule = rules.first() as SplitPlaceholderRule
-        assertEquals(FINISH_ALWAYS, rule.finishPrimaryWithPlaceholder)
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
+            .setLayoutDirection(LOCALE)
+            .setAnimationBackgroundColor(0)
+            .build()
+        assertNull(rule.tag)
+        assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
         assertEquals(false, rule.isSticky)
-        assertEquals(0.5f, rule.splitRatio)
-        assertEquals(LayoutDirection.LOCALE, rule.layoutDirection)
-        assertTrue(rule.checkParentBounds(density, minValidWindowBounds()))
-        assertFalse(rule.checkParentBounds(density, almostValidWindowBounds()))
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertTrue(rule.checkParentBounds(density, validBounds))
+        assertFalse(rule.checkParentBounds(density, invalidBounds))
+    }
+
+    /**
+     * Verifies that horizontal layout are set correctly when reading [SplitPlaceholderRule]
+     * from XML.
+     */
+    @RequiresApi(Build.VERSION_CODES.M)
+    @Test
+    fun testHorizontalLayout_SplitPlaceholderRule_Xml() {
+        val rules = RuleController
+            .parseRules(application, R.xml.test_split_config_split_placeholder_horizontal_layout)
+        assertEquals(1, rules.size)
+        val rule: SplitPlaceholderRule = rules.first() as SplitPlaceholderRule
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
+            .setLayoutDirection(BOTTOM_TO_TOP)
+            .setAnimationBackgroundColor(application.resources.getColor(R.color.testColor, null))
+            .build()
+        assertEquals(TEST_TAG, rule.tag)
+        assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
+        assertEquals(false, rule.isSticky)
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertTrue(rule.checkParentBounds(density, validBounds))
+        assertFalse(rule.checkParentBounds(density, invalidBounds))
     }
 
     /**
@@ -170,12 +250,21 @@
     fun testDefaults_SplitPlaceholderRule_Builder() {
         val rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
             .setMinWidthDp(123)
-            .setMinSmallestWidthDp(456)
+            .setMinHeightDp(456)
+            .setMinSmallestWidthDp(789)
             .build()
-        assertEquals(FINISH_ALWAYS, rule.finishPrimaryWithPlaceholder)
+        assertNull(rule.tag)
+        assertEquals(ALWAYS, rule.finishPrimaryWithPlaceholder)
         assertEquals(false, rule.isSticky)
-        assertEquals(0.5f, rule.splitRatio)
-        assertEquals(LayoutDirection.LOCALE, rule.layoutDirection)
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.5f))
+            .setLayoutDirection(LOCALE)
+            .setAnimationBackgroundColor(0)
+            .build()
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
+        assertEquals(123, rule.minWidthDp)
+        assertEquals(456, rule.minHeightDp)
+        assertEquals(789, rule.minSmallestWidthDp)
     }
 
     /**
@@ -192,22 +281,29 @@
             )
         )
         val intent = Intent("ACTION")
+        val expectedSplitLayout = SplitAttributes.Builder()
+            .setSplitType(SplitAttributes.SplitType.ratio(0.3f))
+            .setLayoutDirection(LEFT_TO_RIGHT)
+            .setAnimationBackgroundColor(Color.GREEN)
+            .build()
         val rule = SplitPlaceholderRule.Builder(filters, intent)
             .setMinWidthDp(123)
-            .setMinSmallestWidthDp(456)
-            .setFinishPrimaryWithPlaceholder(FINISH_ADJACENT)
+            .setMinHeightDp(456)
+            .setMinSmallestWidthDp(789)
+            .setFinishPrimaryWithPlaceholder(ADJACENT)
             .setSticky(true)
-            .setSplitRatio(0.3f)
-            .setLayoutDirection(LayoutDirection.LTR)
+            .setDefaultSplitAttributes(expectedSplitLayout)
+            .setTag(TEST_TAG)
             .build()
-        assertEquals(FINISH_ADJACENT, rule.finishPrimaryWithPlaceholder)
+        assertEquals(ADJACENT, rule.finishPrimaryWithPlaceholder)
         assertEquals(true, rule.isSticky)
-        assertEquals(0.3f, rule.splitRatio)
-        assertEquals(LayoutDirection.LTR, rule.layoutDirection)
+        assertEquals(expectedSplitLayout, rule.defaultSplitAttributes)
         assertEquals(filters, rule.filters)
         assertEquals(intent, rule.placeholderIntent)
         assertEquals(123, rule.minWidthDp)
-        assertEquals(456, rule.minSmallestWidthDp)
+        assertEquals(456, rule.minHeightDp)
+        assertEquals(789, rule.minSmallestWidthDp)
+        assertEquals(TEST_TAG, rule.tag)
     }
 
     /**
@@ -219,34 +315,30 @@
         assertThrows(IllegalArgumentException::class.java) {
             SplitPlaceholderRule.Builder(HashSet(), Intent())
                 .setMinWidthDp(-1)
-                .setMinSmallestWidthDp(456)
+                .setMinHeightDp(456)
+                .setMinSmallestWidthDp(789)
                 .build()
         }
         assertThrows(IllegalArgumentException::class.java) {
             SplitPlaceholderRule.Builder(HashSet(), Intent())
                 .setMinWidthDp(123)
+                .setMinHeightDp(-1)
+                .setMinSmallestWidthDp(789)
+                .build()
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitPlaceholderRule.Builder(HashSet(), Intent())
+                .setMinWidthDp(123)
+                .setMinHeightDp(456)
                 .setMinSmallestWidthDp(-1)
                 .build()
         }
         assertThrows(IllegalArgumentException::class.java) {
             SplitPlaceholderRule.Builder(HashSet(), Intent())
                 .setMinWidthDp(123)
-                .setMinSmallestWidthDp(456)
-                .setFinishPrimaryWithPlaceholder(FINISH_NEVER)
-                .build()
-        }
-        assertThrows(IllegalArgumentException::class.java) {
-            SplitPlaceholderRule.Builder(HashSet(), Intent())
-                .setMinWidthDp(123)
-                .setMinSmallestWidthDp(456)
-                .setSplitRatio(-1.0f)
-                .build()
-        }
-        assertThrows(IllegalArgumentException::class.java) {
-            SplitPlaceholderRule.Builder(HashSet(), Intent())
-                .setMinWidthDp(123)
-                .setMinSmallestWidthDp(456)
-                .setSplitRatio(1.1f)
+                .setMinHeightDp(456)
+                .setMinSmallestWidthDp(789)
+                .setFinishPrimaryWithPlaceholder(NEVER)
                 .build()
         }
     }
@@ -260,7 +352,22 @@
             .parseRules(application, R.xml.test_split_config_default_activity_rule)
         assertEquals(1, rules.size)
         val rule: ActivityRule = rules.first() as ActivityRule
-        assertEquals(false, rule.alwaysExpand)
+        assertNull(rule.tag)
+        assertFalse(rule.alwaysExpand)
+    }
+
+    /**
+     * Verifies that [ActivityRule.tag] and [ActivityRule.alwaysExpand] are set correctly when
+     * reading [ActivityRule] from XML.
+     */
+    @Test
+    fun testSetTagAndAlwaysExpand_ActivityRule_Xml() {
+        val rules = RuleController
+            .parseRules(application, R.xml.test_split_config_activity_rule_with_tag)
+        assertEquals(1, rules.size)
+        val rule: ActivityRule = rules.first() as ActivityRule
+        assertEquals(TEST_TAG, rule.tag)
+        assertTrue(rule.alwaysExpand)
     }
 
     /**
@@ -270,7 +377,7 @@
     @Test
     fun testDefaults_ActivityRule_Builder() {
         val rule = ActivityRule.Builder(HashSet()).build()
-        assertEquals(false, rule.alwaysExpand)
+        assertFalse(rule.alwaysExpand)
     }
 
     /**
@@ -287,8 +394,10 @@
         )
         val rule = ActivityRule.Builder(filters)
             .setAlwaysExpand(true)
+            .setTag(TEST_TAG)
             .build()
-        assertEquals(true, rule.alwaysExpand)
+        assertTrue(rule.alwaysExpand)
+        assertEquals(TEST_TAG, rule.tag)
         assertEquals(filters, rule.filters)
     }
 
@@ -309,4 +418,60 @@
 
         return Rect(0, 0, minValidWidthPx, minValidWidthPx)
     }
+
+    @Test
+    fun testIllegalTag_XML() {
+        assertThrows(IllegalArgumentException::class.java) {
+            RuleController.parseRules(application, R.xml.test_split_config_duplicated_tag)
+        }
+    }
+
+    @Test
+    fun testReplacingRuleWithTag() {
+        var rules = RuleController
+            .parseRules(application, R.xml.test_split_config_activity_rule_with_tag)
+        assertEquals(1, rules.size)
+        var rule = rules.first()
+        assertEquals(TEST_TAG, rule.tag)
+        val staticRule = rule as ActivityRule
+        assertTrue(staticRule.alwaysExpand)
+        ruleController.setRules(rules)
+
+        val filters = HashSet<ActivityFilter>()
+        filters.add(
+            ActivityFilter(
+                ComponentName("a", "b"),
+                "ACTION"
+            )
+        )
+        val rule1 = ActivityRule.Builder(filters)
+            .setAlwaysExpand(true)
+            .setTag(TEST_TAG)
+            .build()
+        ruleController.addRule(rule1)
+
+        rules = ruleController.getRules()
+        assertEquals(1, rules.size)
+        rule = rules.first()
+        assertEquals(rule1, rule)
+
+        val intent = Intent("ACTION")
+        val rule2 = SplitPlaceholderRule.Builder(filters, intent)
+            .setMinWidthDp(123)
+            .setMinHeightDp(456)
+            .setMinSmallestWidthDp(789)
+            .setTag(TEST_TAG)
+            .build()
+
+        ruleController.addRule(rule2)
+
+        rules = ruleController.getRules()
+        assertEquals(1, rules.size)
+        rule = rules.first()
+        assertEquals(rule2, rule)
+    }
+
+    companion object {
+        const val TEST_TAG = "test"
+    }
 }
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/layout/ContextUtilsTest.kt b/window/window/src/androidTest/java/androidx/window/layout/ContextUtilsTest.kt
new file mode 100644
index 0000000..7cb4812
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/layout/ContextUtilsTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.window.layout
+
+import android.app.Activity
+import android.content.ContextWrapper
+import android.inputmethodservice.InputMethodService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.window.layout.util.ContextUtils
+import androidx.window.layout.util.ContextUtils.unwrapUiContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+/**
+ * Instrumentation tests for [ContextUtils].
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ContextUtilsTest {
+
+    @Test
+    fun testUnwrapUiContext_noContextWrapper_activity() {
+        val context = mock(Activity::class.java)
+        assertEquals(context, unwrapUiContext(context))
+    }
+
+    @Test
+    fun testUnwrapUiContext_noContextWrapper_inputMethodService() {
+        val context = mock(InputMethodService::class.java)
+        assertEquals(context, unwrapUiContext(context))
+    }
+
+    @Test
+    fun testUnwrapUiContext_contextWrapper_null() {
+        val contextWrapper = ContextWrapper(null)
+        assertEquals(contextWrapper, unwrapUiContext(contextWrapper))
+    }
+}
\ No newline at end of file
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 16dbdf6..1d86ece 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoTrackerImplTest.kt
@@ -16,21 +16,24 @@
 
 package androidx.window.layout
 
-import android.app.Activity
+import android.content.Context
+import android.os.Build
 import androidx.core.util.Consumer
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.window.TestActivity
 import androidx.window.TestConsumer
+import androidx.window.WindowTestUtils
+import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
 import androidx.window.layout.adapter.WindowBackend
+import java.util.concurrent.Executor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.Job
 import org.junit.Rule
 import org.junit.Test
-import java.util.concurrent.Executor
 
 @OptIn(ExperimentalCoroutinesApi::class)
 public class WindowInfoTrackerImplTest {
@@ -60,6 +63,25 @@
     }
 
     @Test
+    public fun testWindowLayoutFeatures_contextAsListener(): Unit = testScope.runTest {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return@runTest
+        }
+        assumeAtLeastVendorApiLevel(2)
+        val fakeBackend = FakeWindowBackend()
+        val repo = WindowInfoTrackerImpl(WindowMetricsCalculatorCompat, fakeBackend)
+        val collector = TestConsumer<WindowLayoutInfo>()
+
+        val windowContext =
+            WindowTestUtils.createOverlayWindowContext()
+        testScope.launch(Job()) {
+            repo.windowLayoutInfo(windowContext).collect(collector::accept)
+        }
+        fakeBackend.triggerSignal(WindowLayoutInfo(emptyList()))
+        collector.assertValue(WindowLayoutInfo(emptyList()))
+    }
+
+    @Test
     public fun testWindowLayoutFeatures_multicasting(): Unit = testScope.runTest {
         activityScenario.scenario.onActivity { testActivity ->
             val windowMetricsCalculator = WindowMetricsCalculatorCompat
@@ -77,10 +99,44 @@
                 repo.windowLayoutInfo(testActivity).collect(collector::accept)
             }
             fakeBackend.triggerSignal(WindowLayoutInfo(emptyList()))
-            collector.assertValues(WindowLayoutInfo(emptyList()), WindowLayoutInfo(emptyList()))
+            collector.assertValues(
+                WindowLayoutInfo(emptyList()),
+                WindowLayoutInfo(emptyList())
+            )
         }
     }
 
+    @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 fakeBackend = FakeWindowBackend()
+        val repo = WindowInfoTrackerImpl(
+            windowMetricsCalculator,
+            fakeBackend
+        )
+        val collector = TestConsumer<WindowLayoutInfo>()
+        val job = Job()
+
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+
+        launch(job) {
+            repo.windowLayoutInfo(windowContext).collect(collector::accept)
+        }
+        launch(job) {
+            repo.windowLayoutInfo(windowContext).collect(collector::accept)
+        }
+
+        fakeBackend.triggerSignal(WindowLayoutInfo(emptyList()))
+        collector.assertValues(
+            WindowLayoutInfo(emptyList()),
+            WindowLayoutInfo(emptyList())
+        )
+    }
+
     private class FakeWindowBackend : WindowBackend {
 
         private class CallbackHolder(
@@ -100,7 +156,7 @@
         }
 
         override fun registerLayoutChangeCallback(
-            activity: Activity,
+            context: Context,
             executor: Executor,
             callback: Consumer<WindowLayoutInfo>
         ) {
diff --git a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt
index 1cdf333..930cb22 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackendTest.kt
@@ -21,12 +21,18 @@
 import java.util.function.Consumer as JavaConsumer
 import android.annotation.SuppressLint
 import android.app.Activity
+import android.content.Context
 import android.graphics.Rect
 import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
 import androidx.core.util.Consumer
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.window.TestActivity
 import androidx.window.TestConsumer
+import androidx.window.WindowTestUtils
+import androidx.window.WindowTestUtils.Companion.assumeAtLeastVendorApiLevel
+import androidx.window.WindowTestUtils.Companion.assumeBeforeVendorApiLevel
 import androidx.window.core.ConsumerAdapter
 import androidx.window.extensions.layout.FoldingFeature.STATE_FLAT
 import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
@@ -67,6 +73,7 @@
 
     @Test
     public fun testExtensionWindowBackend_delegatesToWindowLayoutComponent() {
+        assumeAtLeastVendorApiLevel(1)
         val component = RequestTrackingWindowComponent()
 
         val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -80,7 +87,34 @@
     }
 
     @Test
+    public fun testExtensionWindowBackend_delegatesToWindowLayoutComponentWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = RequestTrackingWindowComponent()
+
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+        val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, windowContextConsumer)
+        assertTrue(
+            "Expected call with Context: $windowContext",
+            component.hasAddCall(windowContext)
+        )
+    }
+
+    /**
+     * After {@link WindowExtensions#VENDOR_API_LEVEL_2} registerLayoutChangeCallback calls
+     * addWindowLayoutInfoListener(context) instead.
+     * {@link testExtensionWindowBackend_registerAtMostOnceWithContext} verifies the same behavior.
+     */
+    @Test
     public fun testExtensionWindowBackend_registerAtMostOnce() {
+        assumeBeforeVendorApiLevel(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -90,7 +124,39 @@
             backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
             backend.registerLayoutChangeCallback(activity, Runnable::run, mock())
 
-            verify(component).addWindowLayoutInfoListener(eq(activity), any())
+            val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+            verify(component).addWindowLayoutInfoListener(eq(activity), consumerCaptor.capture())
+        }
+    }
+
+    @Test
+    public fun testExtensionWindowBackend_registerAtMostOnceWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = mock<WindowLayoutComponent>()
+
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+        val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+
+        val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, windowContextConsumer)
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, mock())
+        verify(component).addWindowLayoutInfoListener(eq(windowContext), consumerCaptor.capture())
+
+        activityScenario.scenario.onActivity { activity ->
+            val consumer = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+            backend.registerLayoutChangeCallback(activity, Runnable::run, mock())
+            verify(component).addWindowLayoutInfoListener(
+                eq(activity as Context),
+                consumerCaptor.capture()
+            )
         }
     }
 
@@ -101,7 +167,7 @@
         assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
 
         val component = mock<WindowLayoutComponent>()
-        whenever(component.addWindowLayoutInfoListener(any(), any()))
+        whenever(component.addWindowLayoutInfoListener(any<Context>(), any()))
             .thenAnswer { invocation ->
                 val consumer = invocation.getArgument(1) as JavaConsumer<OEMWindowLayoutInfo>
                 consumer.accept(OEMWindowLayoutInfo(emptyList()))
@@ -116,9 +182,41 @@
         }
     }
 
+    @Test
+    public fun testExtensionWindowBackend_translateValuesWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = FakeWindowComponent()
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+        val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+        val windowLayoutInfoFromContext = newTestOEMWindowLayoutInfo(windowContext)
+
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, windowContextConsumer)
+        component.emit(windowLayoutInfoFromContext)
+        windowContextConsumer.assertValue(
+                translate(
+                    windowContext,
+                    windowLayoutInfoFromContext
+                )
+        )
+
+        val consumer = TestConsumer<WindowLayoutInfo>()
+        activityScenario.scenario.onActivity { activity ->
+            val windowLayoutInfoFromActivity = newTestOEMWindowLayoutInfo(activity)
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+            component.emit(newTestOEMWindowLayoutInfo(activity))
+            consumer.assertValues(listOf(translate(activity, windowLayoutInfoFromActivity)))
+        }
+    }
+
     @SuppressLint("NewApi") // java.util.function.Consumer was added in API 24 (N)
     @Test
     public fun testExtensionWindowBackend_infoReplayedForAdditionalListener() {
+        assumeBeforeVendorApiLevel(2)
         assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
 
         val component = mock<WindowLayoutComponent>()
@@ -139,7 +237,43 @@
     }
 
     @Test
+    public fun testExtensionWindowBackend_infoReplayedForAdditionalListenerWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = mock<WindowLayoutComponent>()
+        whenever(component.addWindowLayoutInfoListener(any(), any()))
+            .thenAnswer { invocation ->
+                val consumer = invocation.getArgument(1) as JavaConsumer<OEMWindowLayoutInfo>
+                consumer.accept(OEMWindowLayoutInfo(emptyList()))
+            }
+        whenever(component.addWindowLayoutInfoListener(any<Context>(), any()))
+            .thenAnswer { invocation ->
+                val consumer = invocation.getArgument(1) as JavaConsumer<OEMWindowLayoutInfo>
+                consumer.accept(OEMWindowLayoutInfo(emptyList()))
+            }
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        activityScenario.scenario.onActivity { activity ->
+            val consumer = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(activity, Runnable::run, mock())
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+
+            consumer.assertValue(WindowLayoutInfo(emptyList()))
+        }
+
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+        val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, windowContextConsumer)
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, mock())
+        windowContextConsumer.assertValue(WindowLayoutInfo(emptyList()))
+    }
+
+    @Test
     public fun testExtensionWindowBackend_removeMatchingCallback() {
+        assumeBeforeVendorApiLevel(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -158,6 +292,7 @@
 
     @Test
     public fun testExtensionWindowBackend_removesMultipleCallback() {
+        assumeBeforeVendorApiLevel(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -177,8 +312,107 @@
         }
     }
 
+    /**
+     * Verifies context and consumer registration can be registered with using either
+     * addWindowLayoutInfoListener(context) or addWindowLayoutInfoListener(activity),
+     * but all registration are cleaned up by  removeWindowLayoutInfoListener().
+     * Note: addWindowLayoutInfoListener(context) is added in
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}.
+     */
+    @Test
+    public fun testExtensionWindowBackend_removeMatchingCallbackWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            // createWindowContext is available after R.
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = mock<WindowLayoutComponent>()
+
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        activityScenario.scenario.onActivity { activity ->
+            val consumer = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+            backend.unregisterLayoutChangeCallback(consumer)
+            val windowContext = WindowTestUtils.createOverlayWindowContext()
+            val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(
+                windowContext,
+                Runnable::run,
+                windowContextConsumer
+            )
+            backend.unregisterLayoutChangeCallback(windowContextConsumer)
+
+            val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+            verify(component).addWindowLayoutInfoListener(
+                eq(activity as Context),
+                consumerCaptor.capture()
+            )
+            verify(component).removeWindowLayoutInfoListener(consumerCaptor.firstValue)
+
+            verify(component).addWindowLayoutInfoListener(
+                eq(windowContext),
+                consumerCaptor.capture()
+            )
+            verify(component).removeWindowLayoutInfoListener(consumerCaptor.lastValue)
+        }
+    }
+
+    @Test
+    public fun testExtensionWindowBackend_removeMultipleCallbackWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            // createWindowContext is available after R.
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = mock<WindowLayoutComponent>()
+
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        activityScenario.scenario.onActivity { activity ->
+            val consumer = TestConsumer<WindowLayoutInfo>()
+            val consumer2 = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer2)
+            backend.unregisterLayoutChangeCallback(consumer)
+            backend.unregisterLayoutChangeCallback(consumer2)
+            val windowContext = WindowTestUtils.createOverlayWindowContext()
+            val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+            val windowContextConsumer2 = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(
+                windowContext,
+                Runnable::run,
+                windowContextConsumer
+            )
+            backend.registerLayoutChangeCallback(
+                windowContext,
+                Runnable::run,
+                windowContextConsumer2
+            )
+            backend.unregisterLayoutChangeCallback(windowContextConsumer)
+            backend.unregisterLayoutChangeCallback(windowContextConsumer2)
+
+            val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+            verify(component).addWindowLayoutInfoListener(
+                eq(activity as Context),
+                consumerCaptor.capture()
+            )
+            verify(component).removeWindowLayoutInfoListener(consumerCaptor.firstValue)
+
+            verify(component).addWindowLayoutInfoListener(
+                eq(windowContext),
+                consumerCaptor.capture()
+            )
+            verify(component).removeWindowLayoutInfoListener(consumerCaptor.lastValue)
+            assertFalse(backend.hasRegisteredListeners())
+        }
+    }
+
     @Test
     public fun testExtensionWindowBackend_reRegisterCallback() {
+        assumeBeforeVendorApiLevel(2)
         val component = mock<WindowLayoutComponent>()
 
         val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -198,8 +432,54 @@
         }
     }
 
+    /**
+     * Verifies that a [WindowLayoutInfo] is published to the consumer upon each registration.
+     * Note: addWindowLayoutInfoListener(context) is added in
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    @Test
+    public fun testExtensionWindowBackend_reRegisterCallbackWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = mock<WindowLayoutComponent>()
+
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+        val windowContextConsumer = TestConsumer<WindowLayoutInfo>()
+
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, windowContextConsumer)
+        backend.unregisterLayoutChangeCallback(windowContextConsumer)
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, windowContextConsumer)
+
+        val windowContextCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+        verify(component, times(2)).addWindowLayoutInfoListener(
+            eq(windowContext),
+            windowContextCaptor.capture()
+        )
+        verify(component).removeWindowLayoutInfoListener(windowContextCaptor.firstValue)
+
+        activityScenario.scenario.onActivity { activity ->
+            val consumer = TestConsumer<WindowLayoutInfo>()
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+            backend.unregisterLayoutChangeCallback(consumer)
+            backend.registerLayoutChangeCallback(activity, Runnable::run, consumer)
+
+            val consumerCaptor = argumentCaptor<JavaConsumer<OEMWindowLayoutInfo>>()
+            verify(component, times(2)).addWindowLayoutInfoListener(
+                eq(activity as Context),
+                consumerCaptor.capture()
+            )
+            verify(component).removeWindowLayoutInfoListener(consumerCaptor.firstValue)
+        }
+    }
+
     @Test
     public fun testRegisterLayoutChangeCallback_clearListeners() {
+        assumeBeforeVendorApiLevel(2)
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
             val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -213,12 +493,12 @@
                 { obj: Runnable -> obj.run() },
                 firstConsumer
             )
+
             backend.registerLayoutChangeCallback(
                 activity,
                 { obj: Runnable -> obj.run() },
                 secondConsumer
             )
-
             assertEquals("Expected one registration for same Activity", 1, component.consumers.size)
             // Check unregistering the layout change callback
             backend.unregisterLayoutChangeCallback(firstConsumer)
@@ -227,8 +507,46 @@
         }
     }
 
+    /**
+     * Verifies that both [Activity] and [UiContext] can be independently registered as listeners to
+     * [WindowLayoutInfo].
+     * Note: addWindowLayoutInfoListener(context) is added in
+     * {@link WindowExtensions#VENDOR_API_LEVEL_2}
+     */
+    @Test
+    public fun testRegisterLayoutChangeCallback_clearListenersWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        activityScenario.scenario.onActivity { activity ->
+            val component = FakeWindowComponent()
+            val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+            // Check registering the layout change callback
+            val firstConsumer = mock<Consumer<WindowLayoutInfo>>()
+            val secondConsumer = mock<Consumer<WindowLayoutInfo>>()
+            val thirdConsumer = mock<Consumer<WindowLayoutInfo>>()
+            val windowContext = WindowTestUtils.createOverlayWindowContext()
+
+            backend.registerLayoutChangeCallback(activity, Runnable::run, firstConsumer)
+            backend.registerLayoutChangeCallback(activity, Runnable::run, secondConsumer)
+            backend.registerLayoutChangeCallback(windowContext, Runnable::run, thirdConsumer)
+
+            assertEquals("Expected one registration for same Activity", 2, component.consumers.size)
+            // Check unregistering the layout change callback
+            backend.unregisterLayoutChangeCallback(firstConsumer)
+            backend.unregisterLayoutChangeCallback(secondConsumer)
+            backend.unregisterLayoutChangeCallback(thirdConsumer)
+            assertTrue("Expected all listeners to be removed", component.consumers.isEmpty())
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
     @Test
     public fun testLayoutChangeCallback_emitNewValue() {
+        assumeBeforeVendorApiLevel(2)
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
             val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -245,7 +563,31 @@
     }
 
     @Test
+    public fun testExtensionWindowBackend_emitNewValueWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
+
+        val component = FakeWindowComponent()
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+
+        // Check that callbacks from the extension are propagated for WindowContext.
+        val consumer = mock<Consumer<WindowLayoutInfo>>()
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+        backend.registerLayoutChangeCallback(
+            windowContext, Runnable::run, consumer
+        )
+        val windowLayoutInfo = newTestOEMWindowLayoutInfo(windowContext)
+
+        component.emit(windowLayoutInfo)
+        verify(consumer).accept(translate(windowContext, windowLayoutInfo))
+    }
+
+    @RequiresApi(Build.VERSION_CODES.R)
+    @Test
     public fun testWindowLayoutInfo_updatesOnSubsequentRegistration() {
+        assumeAtLeastVendorApiLevel(1)
         activityScenario.scenario.onActivity { activity ->
             val component = FakeWindowComponent()
             val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
@@ -266,8 +608,36 @@
         }
     }
 
-    internal companion object {
+    @Test
+    public fun testWindowLayoutInfo_updatesOnSubsequentRegistrationWithContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return
+        }
+        assumeAtLeastVendorApiLevel(2)
 
+        val component = FakeWindowComponent()
+        val backend = ExtensionWindowLayoutInfoBackend(component, consumerAdapter)
+        val consumer = TestConsumer<WindowLayoutInfo>()
+        val windowContext = WindowTestUtils.createOverlayWindowContext()
+
+        val oemWindowLayoutInfo = newTestOEMWindowLayoutInfo(windowContext)
+
+        val expected = listOf(
+            translate(windowContext, oemWindowLayoutInfo),
+            translate(windowContext, oemWindowLayoutInfo)
+        )
+
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, consumer)
+        component.emit(newTestOEMWindowLayoutInfo(windowContext))
+        backend.unregisterLayoutChangeCallback(consumer)
+
+        backend.registerLayoutChangeCallback(windowContext, Runnable::run, consumer)
+        component.emit(newTestOEMWindowLayoutInfo(windowContext))
+        backend.unregisterLayoutChangeCallback(consumer)
+        consumer.assertValues(expected)
+    }
+
+    internal companion object {
         private fun newTestOEMWindowLayoutInfo(activity: Activity): OEMWindowLayoutInfo {
             val bounds = WindowMetricsCalculatorCompat.computeCurrentWindowMetrics(activity).bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
@@ -275,6 +645,20 @@
             val displayFeatures = listOf(feature)
             return OEMWindowLayoutInfo(displayFeatures)
         }
+
+        /**
+         * Creates an empty OEMWindowLayoutInfo. Note that before R context needs to be an
+         * [Activity]. After R Context can be an [Activity] or a [UiContext] created with
+         * [Context#createWindowContext] or [InputMethodService].
+         */
+        @RequiresApi(Build.VERSION_CODES.R)
+        private fun newTestOEMWindowLayoutInfo(@UiContext context: Context): OEMWindowLayoutInfo {
+            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)
+            return OEMWindowLayoutInfo(displayFeatures)
+        }
     }
 
     private class RequestTrackingWindowComponent : WindowLayoutComponent {
@@ -288,13 +672,20 @@
             records.add(AddCall(activity))
         }
 
+        override fun addWindowLayoutInfoListener(
+            context: Context,
+            consumer: JavaConsumer<OEMWindowLayoutInfo>
+        ) {
+            records.add(AddCall(context))
+        }
+
         override fun removeWindowLayoutInfoListener(consumer: JavaConsumer<OEMWindowLayoutInfo>) {
         }
 
-        class AddCall(val activity: Activity)
+        class AddCall(val context: Context)
 
-        fun hasAddCall(activity: Activity): Boolean {
-            return records.any { addRecord -> addRecord.activity == activity }
+        fun hasAddCall(context: Context): Boolean {
+            return records.any { addRecord -> addRecord.context == context }
         }
     }
 
@@ -309,6 +700,13 @@
             consumers.add(consumer)
         }
 
+        override fun addWindowLayoutInfoListener(
+            context: Context,
+            consumer: JavaConsumer<OEMWindowLayoutInfo>
+        ) {
+            consumers.add(consumer)
+        }
+
         override fun removeWindowLayoutInfoListener(consumer: JavaConsumer<OEMWindowLayoutInfo>) {
             consumers.remove(consumer)
         }
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 f1d8f53..376cf74 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
@@ -16,26 +16,30 @@
 
 package androidx.window.layout.adapter.extensions
 
+import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
+import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
 import android.graphics.Rect
+import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.window.TestActivity
+import androidx.window.WindowTestUtils
 import androidx.window.core.Bounds
 import androidx.window.extensions.layout.FoldingFeature.STATE_HALF_OPENED
 import androidx.window.extensions.layout.FoldingFeature.TYPE_HINGE
 import androidx.window.layout.FoldingFeature.State.Companion.HALF_OPENED
+import androidx.window.layout.HardwareFoldingFeature
 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 org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
-import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
-import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
-import androidx.window.layout.HardwareFoldingFeature
-import androidx.window.layout.WindowLayoutInfo
 
 class ExtensionsWindowLayoutInfoAdapterTest {
 
@@ -46,12 +50,13 @@
     @Test
     fun testTranslate_foldingFeature() {
         activityScenario.scenario.onActivity { activity ->
-            val bounds = computeCurrentWindowMetrics(activity).bounds
+            val windowMetrics = 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)
             val expected = HardwareFoldingFeature(Bounds(featureBounds), HINGE, HALF_OPENED)
 
-            val actual = ExtensionsWindowLayoutInfoAdapter.translate(activity, oemFeature)
+            val actual = ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, oemFeature)
 
             assertEquals(expected, actual)
         }
@@ -74,14 +79,34 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.S)
     @Test
-    fun testTranslate_foldingFeature_invalidType() {
+    fun testTranslate_windowLayoutInfoFromContext() {
+        assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
         activityScenario.scenario.onActivity { activity ->
             val bounds = 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))
+            val localFeature = HardwareFoldingFeature(Bounds(featureBounds), HINGE, HALF_OPENED)
+            val expected = WindowLayoutInfo(listOf(localFeature))
+
+            val windowContext = WindowTestUtils.createOverlayWindowContext()
+
+            val fromContext = ExtensionsWindowLayoutInfoAdapter.translate(windowContext, oemInfo)
+            assertEquals(expected, fromContext)
+        }
+    }
+
+    @Test
+    fun testTranslate_foldingFeature_invalidType() {
+        activityScenario.scenario.onActivity { activity ->
+            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val bounds = windowMetrics.bounds
+            val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, -1, STATE_HALF_OPENED)
 
-            val actual = ExtensionsWindowLayoutInfoAdapter.translate(activity, oemFeature)
+            val actual = ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, oemFeature)
 
             assertNull(actual)
         }
@@ -90,11 +115,12 @@
     @Test
     fun testTranslate_foldingFeature_invalidState() {
         activityScenario.scenario.onActivity { activity ->
-            val bounds = computeCurrentWindowMetrics(activity).bounds
+            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val bounds = windowMetrics.bounds
             val featureBounds = Rect(0, bounds.centerY(), bounds.width(), bounds.centerY())
             val oemFeature = OEMFoldingFeature(featureBounds, TYPE_HINGE, -1)
 
-            val actual = ExtensionsWindowLayoutInfoAdapter.translate(activity, oemFeature)
+            val actual = ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, oemFeature)
 
             assertNull(actual)
         }
@@ -103,7 +129,8 @@
     @Test
     fun testTranslate_foldingFeature_invalidBounds() {
         activityScenario.scenario.onActivity { activity ->
-            val windowBounds = computeCurrentWindowMetrics(activity).bounds
+            val windowMetrics = computeCurrentWindowMetrics(activity)
+            val windowBounds = windowMetrics.bounds
 
             val source = invalidNonZeroFoldBounds(windowBounds)
                 .map { featureBounds ->
@@ -111,7 +138,7 @@
                 }
 
             val invalidFeatures = source.mapNotNull { feature ->
-                ExtensionsWindowLayoutInfoAdapter.translate(activity, feature)
+                ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, feature)
             }
 
             assertTrue(source.isNotEmpty())
@@ -121,4 +148,4 @@
             )
         }
     }
-}
\ No newline at end of file
+}
diff --git a/window/window-samples/src/main/res/values/colors.xml b/window/window/src/androidTest/res/values/colors.xml
similarity index 62%
copy from window/window-samples/src/main/res/values/colors.xml
copy to window/window/src/androidTest/res/values/colors.xml
index 41a72b2..939eec6 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/window/window/src/androidTest/res/values/colors.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2020 The Android Open Source Project
+  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.
@@ -14,14 +14,6 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-
 <resources>
-    <color name="colorPrimary">#6200EE</color>
-    <color name="colorPrimaryDark">#3700B3</color>
-    <color name="colorAccent">#03DAC5</color>
-
-    <color name="colorFeatureFold">#7700FF00</color>
-
-    <color name="colorSplitContentBackground">#3B6BDB4C</color>
-    <color name="colorSplitControlsBackground">#475ABFF3</color>
-</resources>
+    <color name="testColor">#00FF00</color>
+</resources>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/values/colors.xml b/window/window/src/androidTest/res/xml/test_split_config_activity_rule_with_tag.xml
similarity index 60%
copy from window/window-samples/src/main/res/values/colors.xml
copy to window/window/src/androidTest/res/xml/test_split_config_activity_rule_with_tag.xml
index 41a72b2..d7e0025 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_activity_rule_with_tag.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2020 The Android Open Source Project
+  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.
@@ -14,14 +14,12 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-
-<resources>
-    <color name="colorPrimary">#6200EE</color>
-    <color name="colorPrimaryDark">#3700B3</color>
-    <color name="colorAccent">#03DAC5</color>
-
-    <color name="colorFeatureFold">#7700FF00</color>
-
-    <color name="colorSplitContentBackground">#3B6BDB4C</color>
-    <color name="colorSplitControlsBackground">#475ABFF3</color>
-</resources>
+<resources
+    xmlns:window="http://schemas.android.com/apk/res-auto">
+    <ActivityRule
+        window:tag="test"
+        window:alwaysExpand="true">
+        <ActivityFilter
+            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+    </ActivityRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_default_activity_rule.xml b/window/window/src/androidTest/res/xml/test_split_config_default_activity_rule.xml
index 3ae66aa..e47b93e 100644
--- a/window/window/src/androidTest/res/xml/test_split_config_default_activity_rule.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_default_activity_rule.xml
@@ -18,6 +18,6 @@
     xmlns:window="http://schemas.android.com/apk/res-auto">
     <ActivityRule>
         <ActivityFilter
-            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+            window:activityName="SplitActivityList"/>
     </ActivityRule>
 </resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_default_split_placeholder_rule.xml b/window/window/src/androidTest/res/xml/test_split_config_default_split_placeholder_rule.xml
index ffbc6ca..e67acb3 100644
--- a/window/window/src/androidTest/res/xml/test_split_config_default_split_placeholder_rule.xml
+++ b/window/window/src/androidTest/res/xml/test_split_config_default_split_placeholder_rule.xml
@@ -19,6 +19,6 @@
     <SplitPlaceholderRule
         window:placeholderActivityName="C">
         <ActivityFilter
-            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+            window:activityName="SplitActivityList"/>
     </SplitPlaceholderRule>
 </resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_duplicated_tag.xml b/window/window/src/androidTest/res/xml/test_split_config_duplicated_tag.xml
new file mode 100644
index 0000000..aba63db
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_duplicated_tag.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<resources
+    xmlns:window="http://schemas.android.com/apk/res-auto">
+    <ActivityRule
+        window:tag="test">
+        <ActivityFilter
+            window:activityName="A"/>
+    </ActivityRule>
+    <SplitPairRule
+        window:tag="test">
+        <SplitPairFilter
+            window:primaryActivityName="A"
+            window:secondaryActivityName="B"/>
+    </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml b/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
new file mode 100644
index 0000000..5e75a12
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_split_pair_rule_horizontal_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<resources
+    xmlns:window="http://schemas.android.com/apk/res-auto">
+    <SplitPairRule
+        window:splitRatio="0.3"
+        window:tag="test"
+        window:splitMinWidthDp="0"
+        window:splitMinHeightDp="600"
+        window:splitMinSmallestWidthDp="0"
+        window:splitLayoutDirection="topToBottom"
+        window:animationBackgroundColor="#0000FF">
+        <SplitPairFilter
+            window:primaryActivityName="A"
+            window:secondaryActivityName="B"/>
+    </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml b/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
new file mode 100644
index 0000000..df0871f
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_split_placeholder_horizontal_layout.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<resources
+    xmlns:window="http://schemas.android.com/apk/res-auto">
+    <SplitPlaceholderRule
+        window:tag="test"
+        window:placeholderActivityName="C"
+        window:splitRatio="0.3"
+        window:splitMinWidthDp="0"
+        window:splitMinHeightDp="600"
+        window:splitMinSmallestWidthDp="0"
+        window:splitLayoutDirection="bottomToTop"
+        window:animationBackgroundColor="@color/testColor">
+        <ActivityFilter
+            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+    </SplitPlaceholderRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
new file mode 100644
index 0000000..996c7ad
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/EmptyWindowAreaControllerImpl.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 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.window.area
+
+import android.app.Activity
+import androidx.window.core.ExperimentalWindowApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import java.util.concurrent.Executor
+
+/**
+ * Empty Implementation for devices that do not
+ * support the [WindowAreaController] functionality
+ */
+@ExperimentalWindowApi
+internal class EmptyWindowAreaControllerImpl : WindowAreaController {
+    override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
+        return flowOf(WindowAreaStatus.UNSUPPORTED)
+    }
+
+    override fun rearDisplayMode(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    ) {
+        throw WindowAreaController.REAR_DISPLAY_ERROR
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
similarity index 62%
copy from window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt
copy to window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
index 754e11d8..9a5bbd3 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityListPlaceholder.kt
+++ b/window/window/src/main/java/androidx/window/area/RearDisplaySessionImpl.kt
@@ -14,6 +14,17 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.embedding
+package androidx.window.area
 
-open class SplitActivityListPlaceholder : SplitActivityPlaceholder()
\ No newline at end of file
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+
+@ExperimentalWindowApi
+internal class RearDisplaySessionImpl(
+    private val windowAreaComponent: WindowAreaComponent
+) : WindowAreaSession {
+
+    override fun close() {
+        windowAreaComponent.endRearDisplaySession()
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
new file mode 100644
index 0000000..30ca792
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 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.window.area
+
+import android.app.Activity
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.window.core.BuildConfig
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.VerificationMode
+import androidx.window.extensions.WindowExtensionsProvider
+import androidx.window.extensions.area.WindowAreaComponent
+import java.util.concurrent.Executor
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * An interface to provide the information and behavior around moving windows between
+ * displays or display areas on a device.
+ */
+@ExperimentalWindowApi
+interface WindowAreaController {
+
+    /*
+    Marked with RestrictTo as we iterate and define the
+    Kotlin API we want to provide.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun rearDisplayStatus(): Flow<WindowAreaStatus>
+
+    /*
+    Marked with RestrictTo as we iterate and define the
+    Kotlin API we want to provide.
+     */
+    @Throws(UnsupportedOperationException::class)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun rearDisplayMode(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    )
+
+    public companion object {
+        internal val REAR_DISPLAY_ERROR =
+            UnsupportedOperationException("Rear Display mode cannot be enabled currently")
+
+        private val TAG = WindowAreaController::class.simpleName
+
+        private var decorator: WindowAreaControllerDecorator = EmptyDecorator
+
+        /**
+         * Provides an instance of [WindowAreaController].
+         */
+        @JvmName("getOrCreate")
+        @JvmStatic
+        public fun getOrCreate(): WindowAreaController {
+            var windowAreaComponentExtensions: WindowAreaComponent?
+            try {
+                windowAreaComponentExtensions = WindowExtensionsProvider
+                    .getWindowExtensions()
+                    .windowAreaComponent
+            } catch (t: Throwable) {
+                if (BuildConfig.verificationMode == VerificationMode.STRICT) {
+                    Log.d(TAG, "Failed to load WindowExtensions")
+                }
+                windowAreaComponentExtensions = null
+            }
+            val controller =
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
+                    windowAreaComponentExtensions == null) {
+                    EmptyWindowAreaControllerImpl()
+                } else {
+                    WindowAreaControllerImpl(windowAreaComponentExtensions)
+                }
+            return decorator.decorate(controller)
+        }
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public fun overrideDecorator(overridingDecorator: WindowAreaControllerDecorator) {
+            decorator = overridingDecorator
+        }
+
+        @JvmStatic
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public fun reset() {
+            decorator = EmptyDecorator
+        }
+    }
+}
+
+/**
+ * Decorator that allows us to provide different functionality
+ * in our window-testing artifact.
+ */
+@ExperimentalWindowApi
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface WindowAreaControllerDecorator {
+    /**
+     * Returns an instance of [WindowAreaController] associated to the [Activity]
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun decorate(controller: WindowAreaController): WindowAreaController
+}
+
+@ExperimentalWindowApi
+private object EmptyDecorator : WindowAreaControllerDecorator {
+    override fun decorate(controller: WindowAreaController): WindowAreaController {
+        return controller
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
new file mode 100644
index 0000000..2da99db
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 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.window.area
+
+import android.app.Activity
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.window.core.BuildConfig
+import androidx.window.core.ConsumerAdapter
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.core.VerificationMode
+import androidx.window.extensions.area.WindowAreaComponent
+import java.lang.reflect.InvocationTargetException
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+
+/**
+ * Implementation of WindowAreaController for devices
+ * that do implement the WindowAreaComponent on device.
+ *
+ * Requires [Build.VERSION_CODES.N] due to the use of [Consumer].
+ * Will not be created though on API levels lower than
+ * [Build.VERSION_CODES.S] as that's the min level of support for
+ * this functionality.
+ */
+@ExperimentalWindowApi
+@RequiresApi(Build.VERSION_CODES.N)
+internal class WindowAreaControllerImpl(
+    private val windowAreaComponent: WindowAreaComponent
+) : WindowAreaController {
+
+    private lateinit var rearDisplaySessionConsumer: Consumer<Int>
+    private var currentStatus: WindowAreaStatus? = null
+
+    override fun rearDisplayStatus(): Flow<WindowAreaStatus> {
+        return flow {
+            val channel = Channel<WindowAreaStatus>(
+                capacity = BUFFER_CAPACITY,
+                onBufferOverflow = BufferOverflow.DROP_OLDEST
+            )
+            val listener = Consumer<Int> { status ->
+                currentStatus = WindowAreaStatus.translate(status)
+                channel.trySend(currentStatus ?: WindowAreaStatus.UNSUPPORTED)
+            }
+            val loader = WindowAreaControllerImpl::class.java.classLoader
+            if (loader == null) {
+                channel.trySend(WindowAreaStatus.UNSUPPORTED)
+            } else {
+                val consumerAdapter = ConsumerAdapter(loader)
+                val subscription = consumerAdapter.createSubscriptionNoActivity(
+                    windowAreaComponent,
+                    Int::class,
+                    "addRearDisplayStatusListener",
+                    "removeRearDisplayStatusListener",
+                ) { value ->
+                    listener.accept(value)
+                }
+                try {
+                    for (item in channel) {
+                        emit(item)
+                    }
+                } finally {
+                    subscription.dispose()
+                }
+            }
+        }.distinctUntilChanged()
+    }
+
+    override fun rearDisplayMode(
+        activity: Activity,
+        executor: Executor,
+        windowAreaSessionCallback: WindowAreaSessionCallback
+    ) {
+        // If we already have a status value that is not [WindowAreaStatus.AVAILABLE]
+        // we should throw an exception quick to indicate they tried to enable
+        // RearDisplay mode when it was not available.
+        if (currentStatus != null && currentStatus != WindowAreaStatus.AVAILABLE) {
+            throw WindowAreaController.REAR_DISPLAY_ERROR
+        }
+        rearDisplaySessionConsumer =
+            RearDisplaySessionConsumer(windowAreaSessionCallback, windowAreaComponent)
+        val loader = WindowAreaControllerImpl::class.java.classLoader
+        loader?.let {
+            val consumerAdapter = ConsumerAdapter(it)
+            try {
+                consumerAdapter.createConsumer(
+                    windowAreaComponent,
+                    Int::class,
+                    "startRearDisplaySession",
+                    activity
+                ) { value ->
+                    rearDisplaySessionConsumer.accept(value)
+                }
+            } catch (exception: InvocationTargetException) {
+                // Rethrow the underlying exception when available because Java reflection wraps
+                // the actual exception with InvocationTargetException.
+                throw exception.cause ?: exception
+            }
+        }
+    }
+
+    internal class RearDisplaySessionConsumer(
+        private val appCallback: WindowAreaSessionCallback,
+        private val extensionsComponent: WindowAreaComponent
+    ) : Consumer<Int> {
+
+        private var session: WindowAreaSession? = null
+
+        override fun accept(sessionStatus: Int) {
+            when (sessionStatus) {
+                WindowAreaComponent.SESSION_STATE_ACTIVE -> onSessionStarted()
+                WindowAreaComponent.SESSION_STATE_INACTIVE -> onSessionFinished()
+                else -> {
+                    if (BuildConfig.verificationMode == VerificationMode.STRICT) {
+                        Log.d(TAG, "Received an unknown session status value: $sessionStatus")
+                    }
+                    onSessionFinished()
+                }
+            }
+        }
+
+        private fun onSessionStarted() {
+            session = RearDisplaySessionImpl(extensionsComponent)
+            session?.let { appCallback.onSessionStarted(it) }
+        }
+
+        private fun onSessionFinished() {
+            session = null
+            appCallback.onSessionEnded()
+        }
+    }
+
+    internal companion object {
+        private val TAG = WindowAreaControllerImpl::class.simpleName
+        /*
+        Chosen as 10 for a base default value. We shouldn't be receiving
+        many changes to window area status so this is enough capacity
+        to not end up blocking.
+         */
+        private const val BUFFER_CAPACITY = 10
+    }
+}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
similarity index 68%
copy from window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt
copy to window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
index e69a77b..6cdbd12 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSession.kt
@@ -14,10 +14,16 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.infolog
+package androidx.window.area
+
+import androidx.window.core.ExperimentalWindowApi
 
 /**
- * A data class to hold a title and a detail or subtitle that can be shown using [InfoLogAdapter]
- * . This can be used to create samples with an ordered timeline of events.
+ * Session interface to represent a long-standing
+ * WindowArea mode or feature that provides a handle
+ * to close the session.
  */
-data class InfoLog(val title: String, val detail: String, val id: Int)
\ No newline at end of file
+@ExperimentalWindowApi
+public interface WindowAreaSession {
+    fun close()
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
similarity index 61%
copy from window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt
copy to window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
index e69a77b..80842c4 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/infolog/InfoLog.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaSessionCallback.kt
@@ -14,10 +14,18 @@
  * limitations under the License.
  */
 
-package androidx.window.sample.infolog
+package androidx.window.area
 
-/**
- * A data class to hold a title and a detail or subtitle that can be shown using [InfoLogAdapter]
- * . This can be used to create samples with an ordered timeline of events.
+import androidx.window.core.ExperimentalWindowApi
+
+/** Callback to update the client on the WindowArea Session being
+ * started and ended.
+ * TODO(b/207720511) Move to window-java module when Kotlin API Finalized
  */
-data class InfoLog(val title: String, val detail: String, val id: Int)
\ No newline at end of file
+@ExperimentalWindowApi
+interface WindowAreaSessionCallback {
+
+    fun onSessionStarted(session: WindowAreaSession)
+
+    fun onSessionEnded()
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
new file mode 100644
index 0000000..f60d8f5
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaStatus.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+
+/**
+ * Represents a window area status.
+ */
+@ExperimentalWindowApi
+class WindowAreaStatus private constructor(private val mDescription: String) {
+    override fun toString(): String {
+        return mDescription
+    }
+
+    companion object {
+        /**
+         * Status representing that the WindowArea feature is not a supported
+         * feature on the device.
+         */
+        @JvmField
+        val UNSUPPORTED = WindowAreaStatus("UNSUPPORTED")
+
+        /**
+         * Status representing that the WindowArea feature is currently not available
+         * to be enabled. This could be due to another process has enabled it, or that the
+         * current device configuration doesn't allow it.
+         */
+        @JvmField
+        val UNAVAILABLE = WindowAreaStatus("UNAVAILABLE")
+
+        /**
+         * Status representing that the WindowArea feature is available to be enabled.
+         */
+        @JvmField
+        val AVAILABLE = WindowAreaStatus("AVAILABLE")
+
+        @JvmStatic
+        internal fun translate(status: Int): WindowAreaStatus {
+            return when (status) {
+                WindowAreaComponent.STATUS_AVAILABLE -> AVAILABLE
+                WindowAreaComponent.STATUS_UNAVAILABLE -> UNAVAILABLE
+                else -> UNSUPPORTED
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt b/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt
index e81e16e..af51cd0 100644
--- a/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt
+++ b/window/window/src/main/java/androidx/window/core/ConsumerAdapter.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.SuppressLint
 import android.app.Activity
+import android.content.Context
 import androidx.annotation.CheckResult
 import java.lang.reflect.InvocationHandler
 import java.lang.reflect.Method
@@ -84,6 +85,61 @@
         }
     }
 
+    @CheckResult
+    fun <T : Any> createSubscriptionNoActivity(
+        obj: Any,
+        clazz: KClass<T>,
+        addMethodName: String,
+        removeMethodName: String,
+        consumer: (T) -> Unit
+    ): Subscription {
+        val javaConsumer = buildConsumer(clazz, consumer)
+        obj.javaClass.getMethod(addMethodName, unsafeConsumerClass())
+            .invoke(obj, javaConsumer)
+        val removeMethod = obj.javaClass.getMethod(removeMethodName, unsafeConsumerClass())
+        return object : Subscription {
+            override fun dispose() {
+                removeMethod.invoke(obj, javaConsumer)
+            }
+        }
+    }
+
+    @CheckResult
+    fun <T : Any> createSubscription(
+        obj: Any,
+        clazz: KClass<T>,
+        addMethodName: String,
+        removeMethodName: String,
+        context: Context,
+        consumer: (T) -> Unit
+    ): Subscription {
+        val javaConsumer = buildConsumer(clazz, consumer)
+        obj.javaClass.getMethod(addMethodName, Context::class.java, unsafeConsumerClass())
+            .invoke(obj, context, javaConsumer)
+        val removeMethod = obj.javaClass.getMethod(removeMethodName, unsafeConsumerClass())
+        return object : Subscription {
+            override fun dispose() {
+                removeMethod.invoke(obj, javaConsumer)
+            }
+        }
+    }
+
+    /**
+     * Similar to {@link #createSubscription} but without needing to provide
+     * a {@code removeMethodName} due to it being handled on the extensions side
+     */
+    fun <T : Any> createConsumer(
+        obj: Any,
+        clazz: KClass<T>,
+        addMethodName: String,
+        activity: Activity,
+        consumer: (T) -> Unit
+    ) {
+        val javaConsumer = buildConsumer(clazz, consumer)
+        obj.javaClass.getMethod(addMethodName, Activity::class.java, unsafeConsumerClass())
+            .invoke(obj, activity, javaConsumer)
+        }
+
     private class ConsumerHandler<T : Any>(
         private val clazz: KClass<T>,
         private val consumer: (T) -> Unit
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
index d38b78d..0aa0f09 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
@@ -21,6 +21,7 @@
  * [SplitPairRule].
  */
 class ActivityRule internal constructor(
+    tag: String?,
     /**
      * Filters used to choose when to apply this rule. The rule will be applied if any one of the
      * provided filters matches.
@@ -32,7 +33,7 @@
      * activity that blocks all user interactions, like a warning dialog.
      */
     val alwaysExpand: Boolean = false
-) : EmbeddingRule() {
+) : EmbeddingRule(tag) {
 
     /**
      * Builder for [ActivityRule].
@@ -40,8 +41,9 @@
      * @param filters See [ActivityRule.filters].
      */
     class Builder(
-        private val filters: Set<ActivityFilter>
+        private val filters: Set<ActivityFilter>,
     ) {
+        private var tag: String? = null
         private var alwaysExpand: Boolean = false
 
         /**
@@ -51,7 +53,11 @@
         fun setAlwaysExpand(alwaysExpand: Boolean): Builder =
             apply { this.alwaysExpand = alwaysExpand }
 
-        fun build() = ActivityRule(filters, alwaysExpand)
+        /** @see ActivityRule.tag */
+        fun setTag(tag: String): Builder =
+            apply { this.tag = tag }
+
+        fun build() = ActivityRule(tag, filters, alwaysExpand)
     }
 
     /**
@@ -59,16 +65,14 @@
      * @see filters
      */
     internal operator fun plus(filter: ActivityFilter): ActivityRule {
-        return ActivityRule(
-            filters + filter,
-            alwaysExpand
-        )
+        return ActivityRule(tag, filters + filter, alwaysExpand)
     }
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is ActivityRule) return false
 
+        if (!super.equals(other)) return false
         if (filters != other.filters) return false
         if (alwaysExpand != other.alwaysExpand) return false
 
@@ -76,8 +80,16 @@
     }
 
     override fun hashCode(): Int {
-        var result = filters.hashCode()
+        var result = super.hashCode()
+        result = 31 * result + filters.hashCode()
         result = 31 * result + alwaysExpand.hashCode()
         return result
     }
-}
\ No newline at end of file
+
+    override fun toString(): String {
+        return "ActivityRule:{" +
+            "tag={$tag}," +
+            "filters={$filters}, " +
+            "alwaysExpand={$alwaysExpand}}"
+    }
+}
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 bd90e21..dde5517 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
@@ -69,11 +69,9 @@
         return result
     }
 
-    override fun toString(): String {
-        return buildString {
-            append("ActivityStack{")
-            append("activitiesInProcess=$activitiesInProcess")
-            append("isEmpty=$isEmpty}")
-        }
-    }
-}
\ No newline at end of file
+    override fun toString(): String =
+        "ActivityStack{" +
+            "activitiesInProcess=$activitiesInProcess" +
+            ", isEmpty=$isEmpty" +
+            "}"
+}
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 1700d8d..5f7a579c 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAdapter.kt
@@ -19,6 +19,10 @@
 import androidx.window.extensions.embedding.ActivityRule as OEMActivityRule
 import androidx.window.extensions.embedding.ActivityRule.Builder as ActivityRuleBuilder
 import androidx.window.extensions.embedding.EmbeddingRule as OEMEmbeddingRule
+import androidx.window.extensions.embedding.SplitAttributes as OEMSplitAttributes
+import androidx.window.extensions.embedding.SplitAttributes.SplitType as OEMSplitType
+import androidx.window.extensions.embedding.SplitAttributesCalculator as OEMSplitAttributesCalculator
+import androidx.window.extensions.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams as OEMSplitAttributesCalculatorParams
 import androidx.window.extensions.embedding.SplitInfo as OEMSplitInfo
 import androidx.window.extensions.embedding.SplitPairRule as OEMSplitPairRule
 import androidx.window.extensions.embedding.SplitPairRule.Builder as SplitPairRuleBuilder
@@ -28,9 +32,25 @@
 import android.app.Activity
 import android.content.Context
 import android.content.Intent
+import android.util.LayoutDirection
 import android.view.WindowMetrics
+import androidx.window.core.ExtensionsUtil
 import androidx.window.core.PredicateAdapter
-import androidx.window.extensions.WindowExtensionsProvider
+import androidx.window.embedding.EmbeddingAdapter.VendorApiLevel1Impl.setDefaultSplitAttributesCompat
+import androidx.window.embedding.EmbeddingAdapter.VendorApiLevel1Impl.setFinishPrimaryWithPlaceholderCompat
+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
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
+import androidx.window.embedding.SplitAttributes.SplitType
+import androidx.window.embedding.SplitAttributesCalculator.SplitAttributesCalculatorParams
+import androidx.window.extensions.WindowExtensions
+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.layout.WindowMetricsCalculator
+import androidx.window.layout.adapter.extensions.ExtensionsWindowLayoutInfoAdapter
 
 /**
  * Adapter class that translates data classes between Extension and Jetpack interfaces.
@@ -38,9 +58,10 @@
 internal class EmbeddingAdapter(
     private val predicateAdapter: PredicateAdapter
 ) {
+    private val vendorApiLevel = ExtensionsUtil.safeVendorApiLevel
 
     fun translate(splitInfoList: List<OEMSplitInfo>): List<SplitInfo> {
-        return splitInfoList.map(::translate)
+        return splitInfoList.map(this::translate)
     }
 
     private fun translate(splitInfo: OEMSplitInfo): SplitInfo {
@@ -66,9 +87,53 @@
             secondaryActivityStack.activities,
             isSecondaryStackEmpty
         )
-        return SplitInfo(primaryFragment, secondaryFragment, splitInfo.splitRatio)
+
+        val splitAttributes = if (vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
+            translate(splitInfo.splitAttributes)
+        } else {
+            VendorApiLevel1Impl.getSplitAttributesCompat(splitInfo)
+        }
+        return SplitInfo(primaryFragment, secondaryFragment, splitAttributes)
     }
 
+    private fun translate(splitAttributes: OEMSplitAttributes): SplitAttributes =
+        SplitAttributes.Builder()
+            .setSplitType(translate(splitAttributes.splitType))
+            .setLayoutDirection(
+                when (val layoutDirection = splitAttributes.layoutDirection) {
+                    OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT -> LEFT_TO_RIGHT
+                    OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT -> RIGHT_TO_LEFT
+                    OEMSplitAttributes.LayoutDirection.LOCALE -> LOCALE
+                    OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM -> TOP_TO_BOTTOM
+                    OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP -> BOTTOM_TO_TOP
+                    else -> throw IllegalArgumentException(
+                        "Unknown layout direction: $layoutDirection"
+                    )
+                }
+            )
+            .setAnimationBackgroundColor(splitAttributes.animationBackgroundColor)
+            .build()
+
+    private fun translate(splitType: OEMSplitType): SplitType =
+        when (splitType) {
+            is OEMSplitType.RatioSplitType -> translate(splitType)
+            is OEMSplitType.ExpandContainersSplitType -> SplitType.expandContainers()
+            is OEMSplitType.HingeSplitType -> translate(splitType)
+            else -> throw IllegalArgumentException("Unsupported split type: $splitType")
+        }
+
+    private fun translate(hinge: OEMSplitType.HingeSplitType): SplitType.HingeSplitType =
+        SplitType.splitByHinge(
+            when (val splitType = hinge.fallbackSplitType) {
+                is OEMSplitType.ExpandContainersSplitType -> SplitType.expandContainers()
+                is OEMSplitType.RatioSplitType -> translate(splitType)
+                else -> throw IllegalArgumentException("Unsupported split type: $splitType")
+            }
+        )
+
+    private fun translate(splitRatio: OEMSplitType.RatioSplitType): SplitType.RatioSplitType =
+        SplitType.ratio(splitRatio.ratio)
+
     @SuppressLint("ClassVerificationFailure", "NewApi")
     private fun translateActivityPairPredicates(splitPairFilters: Set<SplitPairFilter>): Any {
         return predicateAdapter.buildPairPredicate(
@@ -90,10 +155,42 @@
     }
 
     @SuppressLint("ClassVerificationFailure", "NewApi")
-    private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any {
-        return predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
+    private fun translateParentMetricsPredicate(context: Context, splitRule: SplitRule): Any =
+        predicateAdapter.buildPredicate(WindowMetrics::class) { windowMetrics ->
             splitRule.checkParentMetrics(context, windowMetrics)
         }
+
+    fun translateSplitAttributesCalculator(
+        calculator: SplitAttributesCalculator
+    ): OEMSplitAttributesCalculator =
+        OEMSplitAttributesCalculator { oemSplitAttributesCalculatorParams ->
+            translateSplitAttributes(
+                calculator.computeSplitAttributesForParams(
+                    translate(oemSplitAttributesCalculatorParams)
+                )
+            )
+        }
+
+    @SuppressLint("NewApi")
+    fun translate(
+        params: OEMSplitAttributesCalculatorParams
+    ): SplitAttributesCalculatorParams = let {
+        val taskWindowMetrics = params.parentWindowMetrics
+        val taskConfiguration = params.parentConfiguration
+        val defaultSplitAttributes = params.defaultSplitAttributes
+        val isDefaultMinSizeSatisfied = params.isDefaultMinSizeSatisfied
+        val windowLayoutInfo = params.parentWindowLayoutInfo
+        val splitRuleTag = params.splitRuleTag
+        val windowMetrics = WindowMetricsCalculator.translateWindowMetrics(taskWindowMetrics)
+
+        SplitAttributesCalculatorParams(
+            windowMetrics,
+            taskConfiguration,
+            translate(defaultSplitAttributes),
+            isDefaultMinSizeSatisfied,
+            ExtensionsWindowLayoutInfoAdapter.translate(windowMetrics, windowLayoutInfo),
+            splitRuleTag,
+        )
     }
 
     @SuppressLint("ClassVerificationFailure", "NewApi")
@@ -110,7 +207,6 @@
         }
     }
 
-    @SuppressLint("WrongConstant") // Converting from Jetpack to Extensions constants
     private fun translateSplitPairRule(
         context: Context,
         rule: SplitPairRule,
@@ -119,21 +215,77 @@
         val builder = SplitPairRuleBuilder::class.java.getConstructor(
             predicateClass,
             predicateClass,
-            predicateClass
+            predicateClass,
         ).newInstance(
             translateActivityPairPredicates(rule.filters),
             translateActivityIntentPredicates(rule.filters),
             translateParentMetricsPredicate(context, rule)
         )
-            .setSplitRatio(rule.splitRatio)
-            .setLayoutDirection(rule.layoutDirection)
+            .safeSetDefaultSplitAttributes(rule.defaultSplitAttributes)
             .setShouldClearTop(rule.clearTop)
-            .setFinishPrimaryWithSecondary(rule.finishPrimaryWithSecondary)
-            .setFinishSecondaryWithPrimary(rule.finishSecondaryWithPrimary)
+            .setFinishPrimaryWithSecondary(translateFinishBehavior(rule.finishPrimaryWithSecondary))
+            .setFinishSecondaryWithPrimary(translateFinishBehavior(rule.finishSecondaryWithPrimary))
+        val tag = rule.tag
+        if (tag != null && vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
+            builder.setTag(tag)
+        }
         return builder.build()
     }
 
-    @SuppressLint("WrongConstant") // Converting from Jetpack to Extensions constants
+    private fun SplitPairRuleBuilder.safeSetDefaultSplitAttributes(
+        defaultAttrs: SplitAttributes
+    ): SplitPairRuleBuilder = apply {
+        if (vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
+            setDefaultSplitAttributes(translateSplitAttributes(defaultAttrs))
+        } else {
+            setDefaultSplitAttributesCompat(this@safeSetDefaultSplitAttributes, defaultAttrs)
+        }
+    }
+
+    private fun translateSplitAttributes(splitAttributes: SplitAttributes): OEMSplitAttributes {
+        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_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()
+            .setSplitType(translateSplitType(splitAttributes.splitType))
+            .setLayoutDirection(
+                when (splitAttributes.layoutDirection) {
+                    LOCALE -> OEMSplitAttributes.LayoutDirection.LOCALE
+                    LEFT_TO_RIGHT -> OEMSplitAttributes.LayoutDirection.LEFT_TO_RIGHT
+                    RIGHT_TO_LEFT -> OEMSplitAttributes.LayoutDirection.RIGHT_TO_LEFT
+                    TOP_TO_BOTTOM -> OEMSplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+                    BOTTOM_TO_TOP -> OEMSplitAttributes.LayoutDirection.BOTTOM_TO_TOP
+                    else -> throw IllegalArgumentException("Unsupported layoutDirection:" +
+                        "$splitAttributes.layoutDirection"
+                    )
+                }
+            )
+            .setAnimationBackgroundColor(splitAttributes.animationBackgroundColor)
+            .build()
+    }
+
+    private fun translateSplitType(splitType: SplitType): OEMSplitType {
+        require(vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2)
+        return when (splitType) {
+            is SplitType.HingeSplitType -> translateHinge(splitType)
+            is SplitType.ExpandContainersSplitType -> OEMSplitType.ExpandContainersSplitType()
+            is SplitType.RatioSplitType -> translateRatio(splitType)
+            else -> throw IllegalArgumentException("Unsupported splitType: $splitType")
+        }
+    }
+
+    private fun translateHinge(hinge: SplitType.HingeSplitType): OEMSplitType.HingeSplitType =
+        OEMSplitType.HingeSplitType(
+            when (val splitType = hinge.fallbackSplitType) {
+                is SplitType.ExpandContainersSplitType -> OEMSplitType.ExpandContainersSplitType()
+                is SplitType.RatioSplitType -> translateRatio(splitType)
+                else -> throw IllegalArgumentException("Unsupported splitType: $splitType")
+            }
+        )
+
+    private fun translateRatio(splitRatio: SplitType.RatioSplitType): OEMSplitType.RatioSplitType =
+        OEMSplitType.RatioSplitType(splitRatio.ratio)
+
     private fun translateSplitPlaceholderRule(
         context: Context,
         rule: SplitPlaceholderRule,
@@ -150,25 +302,46 @@
             translateIntentPredicates(rule.filters),
             translateParentMetricsPredicate(context, rule)
         )
-            .setSplitRatio(rule.splitRatio)
-            .setLayoutDirection(rule.layoutDirection)
             .setSticky(rule.isSticky)
             .safeSetFinishPrimaryWithPlaceholder(rule.finishPrimaryWithPlaceholder)
+            .safeSetDefaultSplitAttributes(rule.defaultSplitAttributes)
+        val tag = rule.tag
+        if (tag != null && vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
+            builder.setTag(tag)
+        }
         return builder.build()
     }
 
-    @Suppress("DEPRECATION")
-    // setFinishPrimaryWithSecondary is to be deprecated but we want to make a safe fallback
-    // behavior here for compatibility reason.
-    // Suppressing deprecation warning to prevent breaking build.
     private fun SplitPlaceholderRuleBuilder.safeSetFinishPrimaryWithPlaceholder(
-        behavior: @SplitPlaceholderRule.SplitPlaceholderFinishBehavior Int
+        behavior: SplitRule.FinishBehavior
     ): SplitPlaceholderRuleBuilder {
-       var extensionApiLevel: Int = WindowExtensionsProvider.getWindowExtensions().vendorApiLevel
-        return if (extensionApiLevel >= 2) {
-            setFinishPrimaryWithPlaceholder(behavior)
+        return if (
+            vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2
+        ) {
+            setFinishPrimaryWithPlaceholder(translateFinishBehavior(behavior))
         } else {
-            setFinishPrimaryWithSecondary(behavior)
+            setFinishPrimaryWithPlaceholderCompat(
+                this@safeSetFinishPrimaryWithPlaceholder,
+                translateFinishBehavior(behavior)
+            )
+        }
+    }
+
+    private fun translateFinishBehavior(behavior: SplitRule.FinishBehavior): Int =
+        when (behavior) {
+            SplitRule.FinishBehavior.NEVER -> FINISH_NEVER
+            SplitRule.FinishBehavior.ALWAYS -> FINISH_ALWAYS
+            SplitRule.FinishBehavior.ADJACENT -> FINISH_ADJACENT
+            else -> throw IllegalArgumentException("Unknown finish behavior:$behavior")
+        }
+
+    private fun SplitPlaceholderRuleBuilder.safeSetDefaultSplitAttributes(
+        defaultAttrs: SplitAttributes
+    ): SplitPlaceholderRuleBuilder = apply {
+        return if (vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
+            setDefaultSplitAttributes(translateSplitAttributes(defaultAttrs))
+        } else {
+            setDefaultSplitAttributesCompat(this@safeSetDefaultSplitAttributes, defaultAttrs)
         }
     }
 
@@ -176,7 +349,7 @@
         rule: ActivityRule,
         predicateClass: Class<*>
     ): OEMActivityRule {
-        return ActivityRuleBuilder::class.java.getConstructor(
+        val builder = ActivityRuleBuilder::class.java.getConstructor(
             predicateClass,
             predicateClass
         ).newInstance(
@@ -184,7 +357,11 @@
             translateIntentPredicates(rule.filters)
         )
             .setShouldAlwaysExpand(rule.alwaysExpand)
-            .build()
+        val tag = rule.tag
+        if (tag != null && vendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2) {
+            builder.setTag(tag)
+        }
+        return builder.build()
     }
 
     fun translate(context: Context, rules: Set<EmbeddingRule>): Set<OEMEmbeddingRule> {
@@ -199,4 +376,77 @@
             }
         }.toSet()
     }
+
+    /**
+     * Provides backward compatibility for Window extensions with
+     * [WindowExtensions.VENDOR_API_LEVEL_1]
+     * @see WindowExtensions.getVendorApiLevel
+     */
+    // Suppress deprecation because this object is to provide backward compatibility.
+    @Suppress("DEPRECATION")
+    private object VendorApiLevel1Impl {
+        fun setFinishPrimaryWithPlaceholderCompat(
+            builder: SplitPlaceholderRuleBuilder,
+            behavior: Int
+        ): SplitPlaceholderRuleBuilder = builder.setFinishPrimaryWithSecondary(behavior)
+
+        fun setDefaultSplitAttributesCompat(
+            builder: SplitPlaceholderRuleBuilder,
+            defaultAttrs: SplitAttributes,
+        ): SplitPlaceholderRuleBuilder {
+            val (splitRatio, layoutDirection) = translateSplitAttributesCompatInternal(defaultAttrs)
+            return builder // #setDefaultAttributes or SplitAttributes ctr weren't supported.
+                .setSplitRatio(splitRatio)
+                .setLayoutDirection(layoutDirection)
+        }
+
+        fun setDefaultSplitAttributesCompat(
+            builder: SplitPairRuleBuilder,
+            defaultAttrs: SplitAttributes,
+        ): SplitPairRuleBuilder {
+            val (splitRatio, layoutDirection) = translateSplitAttributesCompatInternal(defaultAttrs)
+            return builder // #setDefaultAttributes or SplitAttributes ctr weren't supported.
+                .setSplitRatio(splitRatio)
+                .setLayoutDirection(layoutDirection)
+        }
+
+        private fun translateSplitAttributesCompatInternal(
+            attrs: SplitAttributes
+        ): Pair<Float, Int> = // Use a (Float, Integer) pair since SplitAttributes weren't supported
+            if (!isSplitAttributesSupported(attrs)) {
+                // Fallback to expand the secondary container if the SplitAttributes are not
+                // supported.
+                Pair(0.0f, LayoutDirection.LOCALE)
+            } else {
+                Pair(
+                    attrs.splitType.value,
+                    when (attrs.layoutDirection) {
+                        // Legacy LayoutDirection uses LayoutDirection constants in framework APIs.
+                        LOCALE -> LayoutDirection.LOCALE
+                        LEFT_TO_RIGHT -> LayoutDirection.LTR
+                        RIGHT_TO_LEFT -> LayoutDirection.RTL
+                        else -> throw IllegalStateException("Unsupported layout direction must be" +
+                            " covered in @isSplitAttributesSupported!")
+                    }
+                )
+            }
+
+        /**
+         * Returns `true` if `attrs` is compatible with [WindowExtensions.VENDOR_API_LEVEL_1] and
+         * doesn't use the new features introduced in [WindowExtensions.VENDOR_API_LEVEL_2] or
+         * higher.
+         */
+        private fun isSplitAttributesSupported(attrs: SplitAttributes) =
+            attrs.splitType is SplitType.RatioSplitType &&
+                attrs.layoutDirection in arrayOf(LEFT_TO_RIGHT, RIGHT_TO_LEFT, LOCALE)
+
+        /**
+         * Obtains [SplitAttributes] from [OEMSplitInfo] with [WindowExtensions.VENDOR_API_LEVEL_1]
+         */
+        fun getSplitAttributesCompat(splitInfo: OEMSplitInfo): SplitAttributes =
+            SplitAttributes.Builder()
+                .setSplitType(SplitType.buildSplitTypeFromValue(splitInfo.splitRatio))
+                .setLayoutDirection(LOCALE)
+                .build()
+    }
 }
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 539e69d..b54fbe9 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingBackend.kt
@@ -43,4 +43,12 @@
     fun isSplitSupported(): Boolean
 
     fun isActivityEmbedded(activity: Activity): Boolean
+
+    fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator)
+
+    fun clearSplitAttributesCalculator()
+
+    fun getSplitAttributesCalculator(): SplitAttributesCalculator?
+
+    fun isSplitAttributesCalculatorSupported(): Boolean
 }
\ No newline at end of file
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 ea701a1..5013661 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingCompat.kt
@@ -21,7 +21,9 @@
 import android.content.Context
 import android.util.Log
 import androidx.window.core.ConsumerAdapter
+import androidx.window.core.ExtensionsUtil
 import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
+import androidx.window.extensions.WindowExtensions
 import androidx.window.extensions.WindowExtensionsProvider
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent
 import java.lang.reflect.Proxy
@@ -57,6 +59,27 @@
         return embeddingExtension.isActivityEmbedded(activity)
     }
 
+    override fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+        if (!isSplitAttributesCalculatorSupported()) {
+            throw UnsupportedOperationException("#setSplitAttributesCalculator is not supported " +
+                "on the device.")
+        }
+        return embeddingExtension.setSplitAttributesCalculator(
+            adapter.translateSplitAttributesCalculator(calculator)
+        )
+    }
+
+    override fun clearSplitAttributesCalculator() {
+        if (!isSplitAttributesCalculatorSupported()) {
+            throw UnsupportedOperationException("#clearSplitAttributesCalculator is not " +
+                "supported on the device.")
+        }
+        return embeddingExtension.clearSplitAttributesCalculator()
+    }
+
+    override fun isSplitAttributesCalculatorSupported(): Boolean =
+        ExtensionsUtil.safeVendorApiLevel >= WindowExtensions.VENDOR_API_LEVEL_2
+
     companion object {
         const val DEBUG = true
         private const val TAG = "EmbeddingCompat"
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 ff0f50f..2317536 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingInterfaceCompat.kt
@@ -34,4 +34,10 @@
     }
 
     fun isActivityEmbedded(activity: Activity): Boolean
+
+    fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator)
+
+    fun clearSplitAttributesCalculator()
+
+    fun isSplitAttributesCalculatorSupported(): Boolean
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
index e3ff7dd..0f44fcc 100644
--- a/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingRule.kt
@@ -20,4 +20,26 @@
  * Base abstract class for activity embedding presentation rules, such as [SplitPairRule] and
  * [ActivityRule]. Allows grouping different rule types together when updating.
  */
-abstract class EmbeddingRule internal constructor()
+abstract class EmbeddingRule internal constructor(
+    /**
+     * A unique string to identify this [EmbeddingRule], which defaults to `null`.
+     * The suggested usage is to set the tag in the corresponding rule builder to be able to
+     * differentiate between different rules in the callbacks. For example, it can be used to
+     * compute the right [SplitAttributes] for the right split rule in
+     * [SplitAttributesCalculator.computeSplitAttributesForParams].
+     *
+     * @see androidx.window.embedding.RuleController.addRule
+     */
+    val tag: String?
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is EmbeddingRule) return false
+
+        return tag == other.tag
+    }
+
+    override fun hashCode(): Int {
+        return tag.hashCode()
+    }
+}
\ No newline at end of file
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 7fb48ffc..dc1d5a4 100644
--- a/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ExtensionEmbeddingBackend.kt
@@ -21,13 +21,13 @@
 import android.util.Log
 import androidx.annotation.GuardedBy
 import androidx.annotation.VisibleForTesting
+import androidx.collection.ArraySet
 import androidx.core.util.Consumer
 import androidx.window.core.ConsumerAdapter
 import androidx.window.core.ExtensionsUtil
 import androidx.window.core.PredicateAdapter
 import androidx.window.embedding.EmbeddingInterfaceCompat.EmbeddingCallbackInterface
 import java.util.concurrent.CopyOnWriteArrayList
-import java.util.concurrent.CopyOnWriteArraySet
 import java.util.concurrent.Executor
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
@@ -41,6 +41,7 @@
     @VisibleForTesting
     val splitChangeCallbacks: CopyOnWriteArrayList<SplitListenerWrapper>
     private val splitInfoEmbeddingCallback = EmbeddingCallbackImpl()
+    private var splitAttributesCalculator: SplitAttributesCalculator? = null
 
     init {
         splitChangeCallbacks = CopyOnWriteArrayList<SplitListenerWrapper>()
@@ -116,30 +117,109 @@
         }
     }
 
-    private val rules: CopyOnWriteArraySet<EmbeddingRule> =
-        CopyOnWriteArraySet<EmbeddingRule>()
+    @GuardedBy("globalLock")
+    private val ruleTracker = RuleTracker()
 
+    @GuardedBy("globalLock")
     override fun getRules(): Set<EmbeddingRule> {
-        return rules
+        globalLock.withLock { return ruleTracker.splitRules }
     }
 
+    @GuardedBy("globalLock")
     override fun setRules(rules: Set<EmbeddingRule>) {
-        this.rules.clear()
-        this.rules.addAll(rules)
-        embeddingExtension?.setRules(this.rules)
-    }
-
-    override fun addRule(rule: EmbeddingRule) {
-        if (!rules.contains(rule)) {
-            rules.add(rule)
-            embeddingExtension?.setRules(rules)
+        globalLock.withLock {
+            ruleTracker.setRules(rules)
+            embeddingExtension?.setRules(getRules())
         }
     }
 
+    @GuardedBy("globalLock")
+    override fun addRule(rule: EmbeddingRule) {
+        globalLock.withLock {
+            if (rule !in ruleTracker) {
+                ruleTracker.addOrUpdateRule(rule)
+                embeddingExtension?.setRules(getRules())
+            }
+        }
+    }
+
+    @GuardedBy("globalLock")
     override fun removeRule(rule: EmbeddingRule) {
-        if (rules.contains(rule)) {
-            rules.remove(rule)
-            embeddingExtension?.setRules(rules)
+        globalLock.withLock {
+            if (rule in ruleTracker) {
+                ruleTracker.removeRule(rule)
+                embeddingExtension?.setRules(getRules())
+            }
+        }
+    }
+
+    /**
+     * A helper class to manage the registered [tags][EmbeddingRule.tag] and [rules][EmbeddingRule]
+     * It supports:
+     *   - Add a set of [rules][EmbeddingRule] and verify if there's duplicated [EmbeddingRule.tag]
+     *     if needed.
+     *   - Clears all registered [rules][EmbeddingRule]
+     *   - Add a runtime [rule][EmbeddingRule] or update an existing [rule][EmbeddingRule] by
+     *   [tag][EmbeddingRule.tag] if the tag has been registered.
+     *   - Remove a runtime [rule][EmbeddingRule]
+     */
+    private class RuleTracker {
+        val splitRules = ArraySet<EmbeddingRule>()
+        private val tagRuleMap = HashMap<String, EmbeddingRule>()
+
+        fun setRules(rules: Set<EmbeddingRule>) {
+            clearRules()
+            rules.forEach { rule -> addOrUpdateRule(rule, throwOnDuplicateTag = true) }
+        }
+
+        fun clearRules() {
+            splitRules.clear()
+            tagRuleMap.clear()
+        }
+
+        /**
+         * Adds a rule to [RuleTracker] or update an existing rule if the [tag][EmbeddingRule.tag]
+         * has been registered and `throwOnDuplicateTag` is `false`
+         * @throws IllegalArgumentException if `throwOnDuplicateTag` is `true` and the
+         * [tag][EmbeddingRule.tag] has been registered.
+         */
+        fun addOrUpdateRule(rule: EmbeddingRule, throwOnDuplicateTag: Boolean = false) {
+            if (rule in splitRules) {
+                return
+            }
+            val tag = rule.tag
+            if (tag == null) {
+                splitRules.add(rule)
+            } else if (tagRuleMap.containsKey(tag)) {
+                if (throwOnDuplicateTag) {
+                    throw IllegalArgumentException("Duplicated tag: $tag. Tag must be unique " +
+                        "among all registered rules")
+                } else {
+                    // Update the rule if throwOnDuplicateTag = false
+                    val oldRule = tagRuleMap[tag]
+                    splitRules.remove(oldRule)
+                    tagRuleMap[tag] = rule
+                    splitRules.add(rule)
+                }
+            } else {
+                tagRuleMap[tag] = rule
+                splitRules.add(rule)
+            }
+        }
+
+        fun removeRule(rule: EmbeddingRule) {
+            if (rule !in splitRules) {
+                return
+            }
+            splitRules.remove(rule)
+            val tag = rule.tag
+            if (tag != null) {
+                tagRuleMap.remove(rule.tag)
+            }
+        }
+
+        operator fun contains(rule: EmbeddingRule): Boolean {
+            return splitRules.contains(rule)
         }
     }
 
@@ -223,4 +303,24 @@
     override fun isActivityEmbedded(activity: Activity): Boolean {
         return embeddingExtension?.isActivityEmbedded(activity) ?: false
     }
+
+    override fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+        globalLock.withLock {
+            splitAttributesCalculator = calculator
+            embeddingExtension?.setSplitAttributesCalculator(calculator)
+        }
+    }
+
+    override fun clearSplitAttributesCalculator() {
+        globalLock.withLock {
+            splitAttributesCalculator = null
+            embeddingExtension?.clearSplitAttributesCalculator()
+        }
+    }
+
+    override fun getSplitAttributesCalculator(): SplitAttributesCalculator? =
+        globalLock.withLock { splitAttributesCalculator }
+
+    override fun isSplitAttributesCalculatorSupported(): Boolean =
+        embeddingExtension?.isSplitAttributesCalculatorSupported() ?: false
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleController.kt b/window/window/src/main/java/androidx/window/embedding/RuleController.kt
index 867a1a1..fe179cb 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleController.kt
@@ -38,7 +38,8 @@
     private val embeddingBackend: EmbeddingBackend = ExtensionEmbeddingBackend
         .getInstance(applicationContext)
 
-    // TODO(b/258356512): Make this a coroutine API that returns Flow<Set<EmbeddingRule>>.
+    // TODO(b/258356512): Make this API a make this a coroutine API that returns
+    //  Flow<Set<EmbeddingRule>>.
     /**
      * Returns a copy of the currently registered rules.
      */
@@ -47,11 +48,12 @@
     }
 
     /**
-     * Registers a new rule. Will be cleared automatically when the process is stopped.
+     * Registers a new rule, or updates an existing rule if the [tag][EmbeddingRule.tag] has been
+     * registered with [RuleController]. Will be cleared automatically when the process is stopped.
      *
-     * Note that added rules will **not** be applied to any existing split activity
-     * container, and will only be used for new split containers created with future activity
-     * launches.
+     * Note that registering a new rule or updating the existing rule will **not** be applied to any
+     * existing split activity container, and will only be used for new split containers created
+     * with future activity launches.
      *
      * @param rule new [EmbeddingRule] to register.
      */
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 0c81a18..88f3bcb 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
@@ -21,13 +21,12 @@
 import android.content.Intent
 import android.content.res.Resources
 import android.content.res.XmlResourceParser
-import android.util.LayoutDirection
 import androidx.annotation.XmlRes
-
 import androidx.window.R
-import androidx.window.embedding.SplitRule.Companion.FINISH_ALWAYS
-import androidx.window.embedding.SplitRule.Companion.FINISH_NEVER
-
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.getFinishBehaviorFromValue
 import org.xmlpull.v1.XmlPullParser
 
 /**
@@ -66,14 +65,14 @@
                 "SplitPairRule" -> {
                     val splitConfig = parseSplitPairRule(context, parser)
                     lastSplitPairRule = splitConfig
-                    rules.add(lastSplitPairRule)
+                    rules.addRuleWithDuplicatedTagCheck(lastSplitPairRule)
                     lastSplitPlaceholderRule = null
                     lastActivityRule = null
                 }
                 "SplitPlaceholderRule" -> {
                     val placeholderConfig = parseSplitPlaceholderRule(context, parser)
                     lastSplitPlaceholderRule = placeholderConfig
-                    rules.add(lastSplitPlaceholderRule)
+                    rules.addRuleWithDuplicatedTagCheck(lastSplitPlaceholderRule)
                     lastActivityRule = null
                     lastSplitPairRule = null
                 }
@@ -86,11 +85,11 @@
                     val splitFilter = parseSplitPairFilter(context, parser)
                     rules.remove(lastSplitPairRule)
                     lastSplitPairRule += splitFilter
-                    rules.add(lastSplitPairRule)
+                    rules.addRuleWithDuplicatedTagCheck(lastSplitPairRule)
                 }
                 "ActivityRule" -> {
                     val activityConfig = parseActivityRule(context, parser)
-                    rules.add(activityConfig)
+                    rules.addRuleWithDuplicatedTagCheck(activityConfig)
                     lastSplitPairRule = null
                     lastSplitPlaceholderRule = null
                     lastActivityRule = activityConfig
@@ -105,130 +104,171 @@
                     if (lastActivityRule != null) {
                         rules.remove(lastActivityRule)
                         lastActivityRule += activityFilter
-                        rules.add(lastActivityRule)
+                        rules.addRuleWithDuplicatedTagCheck(lastActivityRule)
                     } else if (lastSplitPlaceholderRule != null) {
                         rules.remove(lastSplitPlaceholderRule)
                         lastSplitPlaceholderRule += activityFilter
-                        rules.add(lastSplitPlaceholderRule)
+                        rules.addRuleWithDuplicatedTagCheck(lastSplitPlaceholderRule)
                     }
                 }
             }
             type = parser.next()
         }
-
         return rules
     }
 
+    private fun HashSet<EmbeddingRule>.addRuleWithDuplicatedTagCheck(rule: EmbeddingRule) {
+        val tag = rule.tag
+        forEach { addedRule ->
+            if (tag != null && tag == addedRule.tag) {
+                throw IllegalArgumentException("Duplicated tag: $tag for $rule. " +
+                    "The tag must be unique in XML rule definition.")
+            }
+        }
+        add(rule)
+    }
+
     private fun parseSplitPairRule(
         context: Context,
         parser: XmlResourceParser
-    ): SplitPairRule {
-        val ratio: Float
-        val minWidthDp: Int
-        val minSmallestWidthDp: Int
-        val layoutDir: Int
-        val finishPrimaryWithSecondary: Int
-        val finishSecondaryWithPrimary: Int
-        val clearTop: Boolean
+    ): SplitPairRule =
         context.theme.obtainStyledAttributes(
             parser,
             R.styleable.SplitPairRule,
             0,
             0
-        ).apply {
-            ratio = getFloat(R.styleable.SplitPairRule_splitRatio, 0.5f)
-            minWidthDp = getInteger(
+        ).let { typedArray ->
+            val tag = typedArray.getString(R.styleable.SplitPairRule_tag)
+            val ratio = typedArray.getFloat(R.styleable.SplitPairRule_splitRatio, 0.5f)
+            val minWidthDp = typedArray.getInteger(
                 R.styleable.SplitPairRule_splitMinWidthDp,
                 SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
             )
-            minSmallestWidthDp = getInteger(
+            val minHeightDp = typedArray.getInteger(
+                R.styleable.SplitPairRule_splitMinHeightDp,
+                SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
+            )
+            val minSmallestWidthDp = typedArray.getInteger(
                 R.styleable.SplitPairRule_splitMinSmallestWidthDp,
                 SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
             )
-            layoutDir = getInt(
+            val layoutDir = typedArray.getInt(
                 R.styleable.SplitPairRule_splitLayoutDirection,
-                LayoutDirection.LOCALE
+                LOCALE.value
             )
-            finishPrimaryWithSecondary =
-                getInt(R.styleable.SplitPairRule_finishPrimaryWithSecondary, FINISH_NEVER)
-            finishSecondaryWithPrimary =
-                getInt(R.styleable.SplitPairRule_finishSecondaryWithPrimary, FINISH_ALWAYS)
-            clearTop =
-                getBoolean(R.styleable.SplitPairRule_clearTop, false)
+            val finishPrimaryWithSecondary = typedArray.getInt(
+                R.styleable.SplitPairRule_finishPrimaryWithSecondary,
+                NEVER.value
+            )
+            val finishSecondaryWithPrimary = typedArray.getInt(
+                R.styleable.SplitPairRule_finishSecondaryWithPrimary,
+                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)
+                )
+                .setAnimationBackgroundColor(animationBackgroundColor)
+                .build()
+
+            SplitPairRule.Builder(emptySet())
+                .setTag(tag)
+                .setMinWidthDp(minWidthDp)
+                .setMinHeightDp(minHeightDp)
+                .setMinSmallestWidthDp(minSmallestWidthDp)
+                .setFinishPrimaryWithSecondary(
+                    getFinishBehaviorFromValue(finishPrimaryWithSecondary))
+                .setFinishSecondaryWithPrimary(
+                    getFinishBehaviorFromValue(finishSecondaryWithPrimary))
+                .setClearTop(clearTop)
+                .setDefaultSplitAttributes(defaultAttrs)
+                .build()
         }
-        return SplitPairRule.Builder(emptySet())
-            .setMinWidthDp(minWidthDp)
-            .setMinSmallestWidthDp(minSmallestWidthDp)
-            .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
-            .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
-            .setClearTop(clearTop)
-            .setSplitRatio(ratio)
-            .setLayoutDirection(layoutDir)
-            .build()
-    }
 
     private fun parseSplitPlaceholderRule(
         context: Context,
         parser: XmlResourceParser
-    ): SplitPlaceholderRule {
-        val placeholderActivityIntentName: String?
-        val stickyPlaceholder: Boolean
-        val finishPrimaryWithPlaceholder: Int
-        val ratio: Float
-        val minWidthDp: Int
-        val minSmallestWidthDp: Int
-        val layoutDir: Int
+    ): SplitPlaceholderRule =
         context.theme.obtainStyledAttributes(
             parser,
             R.styleable.SplitPlaceholderRule,
             0,
             0
-        ).apply {
-            placeholderActivityIntentName = getString(
+        ).let { typedArray ->
+            val tag = typedArray.getString(R.styleable.SplitPlaceholderRule_tag)
+            val placeholderActivityIntentName = typedArray.getString(
                 R.styleable.SplitPlaceholderRule_placeholderActivityName
             )
-            stickyPlaceholder = getBoolean(R.styleable.SplitPlaceholderRule_stickyPlaceholder,
-                false)
-            finishPrimaryWithPlaceholder =
-                getInt(R.styleable.SplitPlaceholderRule_finishPrimaryWithPlaceholder, FINISH_ALWAYS)
-            ratio = getFloat(R.styleable.SplitPlaceholderRule_splitRatio, 0.5f)
-            minWidthDp = getInteger(
+            val stickyPlaceholder = typedArray.getBoolean(
+                R.styleable.SplitPlaceholderRule_stickyPlaceholder,
+                false
+            )
+            val finishPrimaryWithPlaceholder = typedArray.getInt(
+                R.styleable.SplitPlaceholderRule_finishPrimaryWithPlaceholder,
+                ALWAYS.value
+            )
+            if (finishPrimaryWithPlaceholder == NEVER.value) {
+                throw IllegalArgumentException(
+                    "Never is not a valid configuration for Placeholder activities. " +
+                        "Please use FINISH_ALWAYS or FINISH_ADJACENT instead or refer to the " +
+                        "current API")
+            }
+            val ratio = typedArray.getFloat(R.styleable.SplitPlaceholderRule_splitRatio, 0.5f)
+            val minWidthDp = typedArray.getInteger(
                 R.styleable.SplitPlaceholderRule_splitMinWidthDp,
                 SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
             )
-            minSmallestWidthDp = getInteger(
+            val minHeightDp = typedArray.getInteger(
+                R.styleable.SplitPlaceholderRule_splitMinHeightDp,
+                SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
+            )
+            val minSmallestWidthDp = typedArray.getInteger(
                 R.styleable.SplitPlaceholderRule_splitMinSmallestWidthDp,
                 SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
             )
-            layoutDir = getInt(
+            val layoutDir = typedArray.getInt(
                 R.styleable.SplitPlaceholderRule_splitLayoutDirection,
-                LayoutDirection.LOCALE
+                LOCALE.value
             )
-        }
-        if (finishPrimaryWithPlaceholder == FINISH_NEVER) {
-                throw IllegalArgumentException(
-                    "FINISH_NEVER is not a valid configuration for Placeholder activities. " +
-                        "Please use FINISH_ALWAYS or FINISH_ADJACENT instead or refer to the " +
-                        "current API")
-        }
-        val packageName = context.applicationContext.packageName
-        val placeholderActivityClassName = buildClassName(
-            packageName,
-            placeholderActivityIntentName
-        )
+            val animationBackgroundColor = typedArray.getColor(
+                R.styleable.SplitPlaceholderRule_animationBackgroundColor,
+                0)
+            typedArray.recycle()
 
-        return SplitPlaceholderRule.Builder(
-            emptySet(),
-            Intent().setComponent(placeholderActivityClassName)
-        )
-            .setMinWidthDp(minWidthDp)
-            .setMinSmallestWidthDp(minSmallestWidthDp)
-            .setSticky(stickyPlaceholder)
-            .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
-            .setSplitRatio(ratio)
-            .setLayoutDirection(layoutDir)
-            .build()
-    }
+            val defaultAttrs = SplitAttributes.Builder()
+                .setSplitType(SplitAttributes.SplitType.buildSplitTypeFromValue(ratio))
+                .setLayoutDirection(
+                    SplitAttributes.LayoutDirection.getLayoutDirectionFromValue(layoutDir)
+                )
+                .setAnimationBackgroundColor(animationBackgroundColor)
+                .build()
+            val packageName = context.applicationContext.packageName
+            val placeholderActivityClassName = buildClassName(
+                packageName,
+                placeholderActivityIntentName
+            )
+
+            SplitPlaceholderRule.Builder(
+                emptySet(),
+                Intent().setComponent(placeholderActivityClassName)
+            )
+                .setTag(tag)
+                .setMinWidthDp(minWidthDp)
+                .setMinHeightDp(minHeightDp)
+                .setMinSmallestWidthDp(minSmallestWidthDp)
+                .setSticky(stickyPlaceholder)
+                .setFinishPrimaryWithPlaceholder(
+                    getFinishBehaviorFromValue(finishPrimaryWithPlaceholder))
+                .setDefaultSplitAttributes(defaultAttrs)
+                .build()
+        }
 
     private fun parseSplitPairFilter(
         context: Context,
@@ -264,18 +304,23 @@
     private fun parseActivityRule(
         context: Context,
         parser: XmlResourceParser
-    ): ActivityRule {
-        val alwaysExpand: Boolean
+    ): ActivityRule =
         context.theme.obtainStyledAttributes(
             parser,
             R.styleable.ActivityRule,
             0,
             0
-        ).apply {
-            alwaysExpand = getBoolean(R.styleable.ActivityRule_alwaysExpand, false)
+        ).let { typedArray ->
+            val tag = typedArray.getString(R.styleable.ActivityRule_tag)
+            val alwaysExpand = typedArray.getBoolean(R.styleable.ActivityRule_alwaysExpand, false)
+            typedArray.recycle()
+
+            val builder = ActivityRule.Builder(emptySet()).setAlwaysExpand(alwaysExpand)
+            if (tag != null) {
+                builder.setTag(tag)
+            }
+            builder.build()
         }
-        return ActivityRule.Builder(emptySet()).setAlwaysExpand(alwaysExpand).build()
-    }
 
     private fun parseActivityFilter(
         context: Context,
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
new file mode 100644
index 0000000..a05dd47
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributes.kt
@@ -0,0 +1,535 @@
+/*
+ * 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.window.embedding
+
+import android.annotation.SuppressLint
+import androidx.annotation.ColorInt
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.window.core.SpecificationComputer.Companion.startSpecification
+import androidx.window.core.VerificationMode
+import androidx.window.embedding.SplitAttributes.LayoutDirection
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.LOCALE
+import androidx.window.embedding.SplitAttributes.SplitType
+import androidx.window.embedding.SplitAttributes.SplitType.Companion.splitEqually
+
+/**
+ * Attributes that describe how the parent window (typically the activity task
+ * window) is split between the primary and secondary activity containers,
+ * including:
+ *   - Split type &mdash; Categorizes the split and specifies the sizes of the
+ *     primary and secondary activity containers relative to the parent bounds
+ *   - Layout direction &mdash; Specifies whether the parent window is split
+ *     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 &mdash; The color of the background during
+ *     animation of the split involving this `SplitAttributes` object if the
+ *     animation requires a background
+ *
+ * Attributes can be configured by:
+ *   - Setting the default `SplitAttributes` using
+ *     [SplitPairRule.Builder.setDefaultSplitAttributes] or
+ *     [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
+ *   - Setting `splitRatio`, `splitLayoutDirection`, and
+ *     `animationBackgroundColor` attributes in `<SplitPairRule>` or
+ *     `<SplitPlaceholderRule>` tags in an XML configuration file. The
+ *     attributes are parsed as [SplitType], [LayoutDirection], and [ColorInt],
+ *     respectively. Note that [SplitType.HingeSplitType] is not supported XML
+ *     format.
+ *   - Using
+ *     [SplitAttributesCalculator.computeSplitAttributesForParams] to customize
+ *     the `SplitAttributes` for a given device and window state.
+ *
+ * @see SplitAttributes.SplitType
+ * @see SplitAttributes.LayoutDirection
+ */
+class SplitAttributes internal constructor(
+
+    /**
+     * The split type attribute. Defaults to an equal split of the parent window
+     * for the primary and secondary containers.
+     */
+    val splitType: SplitType = splitEqually(),
+
+    /**
+     * The layout direction attribute for the parent window split. The default
+     * is based on locale.
+     */
+    val layoutDirection: LayoutDirection = LOCALE,
+
+    /**
+     * The [ColorInt] to use for the background color during the animation of
+     * the split involving this `SplitAttributes` object if the animation
+     * requires a background.
+     *
+     * The default is 0, which specifies the theme window background color.
+     */
+    @ColorInt
+    val animationBackgroundColor: Int = 0
+) {
+
+    /**
+     * The type of parent window split, which defines the proportion of the
+     * parent window occupied by the primary and secondary activity containers.
+     */
+    open class SplitType internal constructor(
+
+        /**
+         * The description of this `SplitType`.
+         */
+        internal val description: String,
+
+        /**
+         * An identifier for the split type.
+         *
+         * Used in the evaluation in the `equals()` method.
+         */
+        internal val value: Float,
+
+    ) {
+
+        /**
+         * A string representation of this split type.
+         *
+         * @return The string representation of the object.
+         */
+        override fun toString(): String = description
+
+        /**
+         * Determines whether this object is the same type of split as the
+         * compared object.
+         *
+         * @param other The object to compare to this object.
+         * @return True if the objects are the same split type, false otherwise.
+         */
+        override fun equals(other: Any?): Boolean {
+            if (other === this) return true
+            if (other !is SplitType) return false
+            return value == other.value &&
+                description == other.description
+        }
+
+        /**
+         * Returns a hash code for this split type.
+         *
+         * @return The hash code for this object.
+         */
+        override fun hashCode(): Int = description.hashCode() + 31 * value.hashCode()
+
+        /**
+         * A window split that's based on the ratio of the size of the primary
+         * container to the size of the parent window.
+         *
+         * @see SplitAttributes.SplitType.ratio
+         */
+        class RatioSplitType internal constructor(
+
+            /**
+             * The proportion of the parent window occupied by the primary
+             * container of the split.
+             */
+            @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+            val ratio: Float
+
+        ) : SplitType("ratio:$ratio", ratio)
+
+        /**
+         * A window split in which the primary and secondary activity containers
+         * each occupy the entire parent window.
+         *
+         * The secondary container overlays the primary container.
+         *
+         * @see SplitAttributes.SplitType.ExpandContainersSplitType
+         */
+        class ExpandContainersSplitType internal constructor() : SplitType("expandContainer", 0.0f)
+
+        /**
+         * A parent window split that conforms to a hinge or separating fold in
+         * the device display.
+         *
+         * @see SplitAttributes.SplitType.splitByHinge
+         */
+        class HingeSplitType internal constructor(
+
+            /**
+             * The split type to use if a split based on the device hinge or
+             * separating fold cannot be determined.
+             */
+            val fallbackSplitType: SplitType
+
+        ) : SplitType("hinge, fallback=$fallbackSplitType", -1.0f)
+
+        /**
+         * Methods that create various split types.
+         */
+        companion object {
+
+            /**
+             * Creates a split type based on the proportion of the parent window
+             * occupied by the primary container of the split.
+             *
+             * 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:
+             * - 0.5 &mdash; Primary container occupies half of the parent
+             *   window; secondary container, the other half
+             * - &gt; 0.5 &mdash; Primary container occupies a larger proportion
+             *   of the parent window than the secondary container
+             * - &lt; 0.5 &mdash; Primary container occupies a smaller
+             *   proportion of the parent window than the secondary container
+             *
+             * @param ratio The proportion of the parent window occupied by the
+             *     primary container of the split.
+             * @return An instance of [RatioSplitType] with the specified ratio.
+             */
+            @JvmStatic
+            fun ratio(
+                @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+                ratio: Float
+            ): RatioSplitType {
+                val checkedRatio = ratio.startSpecification(
+                    TAG,
+                    VerificationMode.STRICT
+                ).require("Ratio must be in range (0.0, 1.0). " +
+                    "Use SplitType.expandContainers() instead of 0 or 1.") {
+                    ratio in 0.0..1.0 && ratio !in arrayOf(0.0f, 1.0f)
+                }.compute()!!
+                return RatioSplitType(checkedRatio)
+            }
+
+            private val EXPAND_CONTAINERS = ExpandContainersSplitType()
+
+            /**
+             * Creates a split type in which the primary and secondary activity
+             * containers each expand to fill the parent window; the secondary
+             * container overlays the primary container.
+             *
+             * Use this method with [SplitAttributesCalculator] to expand the
+             * activity containers in some device states. The following sample
+             * shows how to always fill the parent bounds if the device is in
+             * portrait orientation:
+             *
+             * @sample androidx.window.samples.embedding.expandContainersInPortrait
+             *
+             * @return An instance of [ExpandContainersSplitType].
+             */
+            @JvmStatic
+            fun expandContainers(): ExpandContainersSplitType = EXPAND_CONTAINERS
+
+            /**
+             * Creates a split type in which the primary and secondary
+             * containers occupy equal portions of the parent window.
+             *
+             * Serves as the default [SplitType].
+             *
+             * @return A `RatioSplitType` in which the activity containers
+             *     occupy equal portions of the parent window.
+             */
+            @JvmStatic
+            fun splitEqually(): RatioSplitType = ratio(0.5f)
+
+            /**
+             * Creates a split type in which the split ratio conforms to the
+             * position of a hinge or separating fold in the device display.
+             *
+             * The split type is created only if:
+             * <ul>
+             *     <li>The host task is not in multi-window mode (e.g.,
+             *         split-screen mode or picture-in-picture mode)</li>
+             *     <li>The device has a hinge or separating fold reported by
+             *         [androidx.window.layout.FoldingFeature.isSeparating]</li>
+             *     <li>The hinge or separating fold orientation matches how the
+             *         parent bounds are split:
+             *         <ul style="list-style-type: circle;">
+             *             <li>The hinge or fold orientation is vertical, and
+             *                 the parent bounds are also split vertically
+             *                 (containers are side by side)</li>
+             *             <li>The hinge or fold orientation is horizontal, and
+             *                 the parent bounds are also split horizontally
+             *                 (containers are top and bottom)</li>
+             *         </ul>
+             *     </li>
+             * </ul>
+             *
+             * Otherwise, the method falls back to `fallbackSplitType`.
+             *
+             * @param fallbackSplitType The split type to use if a split based
+             *     on the device hinge or separating fold cannot be determined.
+             *     Can be a [RatioSplitType] or [ExpandContainersSplitType].
+             *     Defaults to [SplitType.splitEqually].
+             * @return An instance of [HingeSplitType] with a fallback split
+             *     type.
+             */
+            @JvmStatic
+            fun splitByHinge(
+                fallbackSplitType: SplitType = splitEqually()
+            ): HingeSplitType {
+                val checkedType = fallbackSplitType.startSpecification(
+                    TAG,
+                    VerificationMode.STRICT
+                ).require(
+                    "FallbackSplitType must be a RatioSplitType or ExpandContainerSplitType"
+                ) {
+                    fallbackSplitType is RatioSplitType ||
+                        fallbackSplitType is ExpandContainersSplitType
+                }.compute()!!
+                return HingeSplitType(checkedType)
+            }
+
+            /**
+             * Returns a `SplitType` with the given `value`.
+             */
+            @SuppressLint("Range") // value = 0.0 is covered.
+            @JvmStatic
+            internal fun buildSplitTypeFromValue(
+                @FloatRange(from = 0.0, to = 1.0, toInclusive = false) value: Float
+            ) = if (value == EXPAND_CONTAINERS.value) {
+                    expandContainers()
+                } else {
+                    ratio(value)
+                }
+        }
+    }
+
+    /**
+     * The layout direction of the primary and secondary activity containers.
+     */
+    class LayoutDirection private constructor(
+
+        /**
+         * The description of this `LayoutDirection`.
+         */
+        private val description: String,
+
+        /**
+         * The enum value defined in `splitLayoutDirection` attributes in
+         * `attrs.xml`.
+         */
+        internal val value: Int,
+
+    ) {
+
+        /**
+         * A string representation of this `LayoutDirection`.
+         *
+         * @return The string representation of the object.
+         */
+        override fun toString(): String = description
+
+        /**
+         * Non-public properties and methods.
+         */
+        companion object {
+            /**
+             * Specifies that the parent bounds are split vertically (side to
+             * side).
+             *
+             * The direction of the primary and secondary containers is deduced
+             * from the locale as either `LEFT_TO_RIGHT` or `RIGHT_TO_LEFT`.
+             *
+             * See also [layoutDirection].
+             */
+            @JvmField
+            val LOCALE = LayoutDirection("LOCALE", 0)
+            /**
+             * Specifies that the parent bounds are split vertically (side to
+             * side).
+             *
+             * Places the primary container in the left portion of the parent
+             * window, and the secondary container in the right portion.
+             *
+             * <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_ltr.png" alt="Activity A starts activity B to the right."/>
+             *
+             * See also [layoutDirection].
+             */
+            @JvmField
+            val LEFT_TO_RIGHT = LayoutDirection("LEFT_TO_RIGHT", 1)
+            /**
+             * Specifies that the parent bounds are split vertically (side to
+             * side).
+             *
+             * Places the primary container in the right portion of the parent
+             * window, and the secondary container in the left portion.
+             *
+             * <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_rtl.png" alt="Activity A starts activity B to the left."/>
+             *
+             * See also [layoutDirection].
+             */
+            @JvmField
+            val RIGHT_TO_LEFT = LayoutDirection("RIGHT_TO_LEFT", 2)
+            /**
+             * Specifies that the parent bounds are split horizontally (top and
+             * bottom).
+             *
+             * Places the primary container in the top portion of the parent
+             * window, and the secondary container in the bottom portion.
+             *
+             * <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_ttb.png" alt="Activity A starts activity B to the bottom."/>
+             *
+             * If the horizontal layout direction is not supported on the
+             * device, layout direction falls back to `LOCALE`.
+             *
+             * See also [layoutDirection].
+             */
+            @JvmField
+            val TOP_TO_BOTTOM = LayoutDirection("TOP_TO_BOTTOM", 3)
+            /**
+             * Specifies that the parent bounds are split horizontally (top and
+             * bottom).
+             *
+             * Places the primary container in the bottom portion of the parent
+             * window, and the secondary container in the top portion.
+             *
+             * <img width="70%" height="70%" src="/images/guide/topics/large-screens/activity-embedding/reference-docs/a_to_a_b_btt.png" alt="Activity A starts activity B to the top."/>
+             *
+             * If the horizontal layout direction is not supported on the
+             * device, layout direction falls back to `LOCALE`.
+             *
+             * See also [layoutDirection].
+             */
+            @JvmField
+            val BOTTOM_TO_TOP = LayoutDirection("BOTTOM_TO_TOP", 4)
+
+            /**
+             * Returns `LayoutDirection` with the given `value`.
+             */
+            @JvmStatic
+            internal fun getLayoutDirectionFromValue(
+                @IntRange(from = 0, to = 4) value: Int
+            ) = when (value) {
+                LEFT_TO_RIGHT.value -> LEFT_TO_RIGHT
+                RIGHT_TO_LEFT.value -> RIGHT_TO_LEFT
+                LOCALE.value -> LOCALE
+                TOP_TO_BOTTOM.value -> TOP_TO_BOTTOM
+                BOTTOM_TO_TOP.value -> BOTTOM_TO_TOP
+                else -> throw IllegalArgumentException("Undefined value:$value")
+            }
+        }
+    }
+
+    /**
+     * Non-public properties and methods.
+     */
+    companion object {
+        private val TAG = SplitAttributes::class.java.simpleName
+    }
+
+    /**
+     * Returns a hash code for this `SplitAttributes` object.
+     *
+     * @return The hash code for this object.
+     */
+    override fun hashCode(): Int {
+        var result = splitType.hashCode()
+        result = result * 31 + layoutDirection.hashCode()
+        result = result * 31 + animationBackgroundColor.hashCode()
+        return result
+    }
+
+    /**
+     * Determines whether this object has the same split attributes as the
+     * compared object.
+     *
+     * @param other The object to compare to this object.
+     * @return True if the objects have the same split attributes, false
+     * otherwise.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SplitAttributes) return false
+        return splitType == other.splitType &&
+            layoutDirection == other.layoutDirection &&
+            animationBackgroundColor == other.animationBackgroundColor
+    }
+
+    /**
+     * A string representation of this `SplitAttributes` object.
+     *
+     * @return The string representation of the object.
+     */
+    override fun toString(): String =
+        "${SplitAttributes::class.java.simpleName}:" +
+            "{splitType=$splitType, layoutDir=$layoutDirection," +
+            " animationBackgroundColor=${Integer.toHexString(animationBackgroundColor)}"
+
+    /**
+     * Builder for creating an instance of [SplitAttributes].
+     *
+     * 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 0, which specifies the theme window
+     * background color.
+     */
+    class Builder {
+        private var splitType: SplitType = splitEqually()
+        private var layoutDirection = LOCALE
+        @ColorInt
+        private var animationBackgroundColor = 0
+
+        /**
+         * Sets the split type attribute.
+         *
+         * The default is an equal split between primary and secondary
+         * containers.
+         *
+         * @param type The split type attribute.
+         * @return This `Builder`.
+         *
+         * @see SplitAttributes.SplitType
+         */
+        fun setSplitType(type: SplitType): Builder = apply { splitType = type }
+
+        /**
+         * Sets the split layout direction attribute.
+         *
+         * The default is based on locale.
+         *
+         * @param layoutDirection The layout direction attribute.
+         * @return This `Builder`.
+         *
+         * @see SplitAttributes.LayoutDirection
+         */
+        fun setLayoutDirection(layoutDirection: LayoutDirection): Builder =
+            apply { this.layoutDirection = layoutDirection }
+
+        /**
+         * Sets the [ColorInt] to use for the background color during animation
+         * of the split involving this `SplitAttributes` object if the animation
+         * requires a background.
+         *
+         * The default is 0, which specifies the theme window background color.
+         *
+         * @param color A packed color int of the form `AARRGGBB`, for the
+         * animation background color.
+         * @return This `Builder`.
+         *
+         * @see SplitAttributes.animationBackgroundColor
+         */
+        fun setAnimationBackgroundColor(@ColorInt color: Int): Builder =
+            apply { this.animationBackgroundColor = color }
+
+        /**
+         * Builds a `SplitAttributes` instance with the attributes specified by
+         * [setSplitType], [setLayoutDirection], and
+         * [setAnimationBackgroundColor].
+         *
+         * @return The new `SplitAttributes` instance.
+         */
+        fun build(): SplitAttributes = SplitAttributes(splitType, layoutDirection,
+            animationBackgroundColor)
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt
new file mode 100644
index 0000000..77720a8
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/SplitAttributesCalculator.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.window.embedding
+
+import android.content.res.Configuration
+import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
+
+/**
+ * A developer-defined [SplitAttributes] calculator to compute the current [SplitAttributes] with
+ * the current device and window state if it is registered via
+ * [SplitController.setSplitAttributesCalculator]. Then
+ * [computeSplitAttributesForParams] will be called when there's
+ * - An activity is started and matches a registered [SplitRule].
+ * - There's a parent configuration update and there's an existing split pair.
+ *
+ * By default, [SplitRule.defaultSplitAttributes] are applied if the parent container's
+ * [WindowMetrics] satisfies the [SplitRule]'s minimum dimensions requirements, which are
+ * [SplitRule.minWidthDp], [SplitRule.minHeightDp] and [SplitRule.minSmallestWidthDp].
+ * The [SplitRule.defaultSplitAttributes] can be set by
+ * - [SplitRule] Builder APIs, which are
+ *   [SplitPairRule.Builder.setDefaultSplitAttributes] and
+ *   [SplitPlaceholderRule.Builder.setDefaultSplitAttributes].
+ * - Specifying with `splitRatio` and `splitLayoutDirection` attributes in `<SplitPairRule>` or
+ * `<SplitPlaceHolderRule>` tags in XML files.
+ *
+ * However, developers may want to apply different [SplitAttributes] for different device or window
+ * states. For example, on foldable devices, developers may want to split the screen vertically if
+ * the device is in landscape, fill the screen if the device is in portrait and split the screen
+ * horizontally if the device is in
+ * [tabletop posture](https://developer.android.com/guide/topics/ui/foldables#postures).
+ * In this case, the [SplitAttributes] can be customized by this callback, which takes effects after
+ * calling [SplitController.setSplitAttributesCalculator]. Developers can also
+ * clear the callback by [SplitController.clearSplitAttributesCalculator].
+ * Then, developers could implement [computeSplitAttributesForParams] as the sample linked below
+ * shows.
+ *
+ * **Note** that [SplitController.setSplitAttributesCalculator] and
+ * [SplitController.clearSplitAttributesCalculator] are only supported if
+ * [SplitController.isSplitAttributesCalculatorSupported] reports `true`. It's
+ * callers' responsibility to check if [SplitAttributesCalculator] is supported by
+ * [SplitController.isSplitAttributesCalculatorSupported] before using the
+ * [SplitAttributesCalculator] feature. It is suggested to always set meaningful
+ * [SplitRule.defaultSplitAttributes] in case [SplitAttributesCalculator] is not supported on
+ * some devices.
+ *
+ * @sample androidx.window.samples.embedding.splitAttributesCalculatorSample
+ *
+ * @see androidx.window.embedding.SplitRule.defaultSplitAttributes
+ * @see androidx.window.embedding.SplitController.getSplitAttributesCalculator
+ */
+interface SplitAttributesCalculator {
+    /**
+     * Computes the [SplitAttributes] with the current device and window states.
+     * @param params See [SplitAttributesCalculatorParams]
+     */
+    fun computeSplitAttributesForParams(params: SplitAttributesCalculatorParams): SplitAttributes
+
+    /** The container of [SplitAttributesCalculator] parameters */
+    class SplitAttributesCalculatorParams internal constructor(
+        /** The parent container's [WindowMetrics] */
+        val parentWindowMetrics: WindowMetrics,
+        /** The parent container's [Configuration] */
+        val parentConfiguration: Configuration,
+        /**
+         * The [SplitRule.defaultSplitAttributes]. It could be from [SplitRule] Builder APIs
+         * ([SplitPairRule.Builder.setDefaultSplitAttributes] or
+         * [SplitPlaceholderRule.Builder.setDefaultSplitAttributes]) or from the `splitRatio` and
+         * `splitLayoutDirection` attributes from static rule definitions.
+         */
+        val defaultSplitAttributes: SplitAttributes,
+        /**
+         * Whether the [parentWindowMetrics] are larger than [SplitRule]'s minimum size criteria,
+         * which are [SplitRule.minWidthDp], [SplitRule.minHeightDp] and
+         * [SplitRule.minSmallestWidthDp]
+         */
+        @get: JvmName("isDefaultMinSizeSatisfied")
+        val isDefaultMinSizeSatisfied: Boolean,
+        /** The parent container's [WindowLayoutInfo] */
+        val parentWindowLayoutInfo: WindowLayoutInfo,
+        /**
+         * The [tag of `SplitRule`][SplitRule.tag] to apply this [SplitAttributes], which is `null`
+         * if the tag is not set.
+         *
+         * @see SplitPairRule.Builder.setTag
+         * @see SplitPlaceholderRule.Builder.setTag
+         */
+        val splitRuleTag: String?,
+    ) {
+        override fun toString(): String =
+            "${SplitAttributesCalculatorParams::class.java.simpleName}:{" +
+                "windowMetrics=$parentWindowMetrics" +
+                ", configuration=$parentConfiguration" +
+                ", windowLayoutInfo=$parentWindowLayoutInfo" +
+                ", defaultSplitAttributes=$defaultSplitAttributes" +
+                ", isDefaultMinSizeSatisfied=$isDefaultMinSizeSatisfied" +
+                ", tag=$splitRuleTag}"
+    }
+}
\ No newline at end of file
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 4825dc0..1073f44 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -24,17 +24,17 @@
 import kotlin.concurrent.withLock
 
 /**
- * A singleton controller class that gets information about the currently active activity
- * splits and provides interaction points to customize the splits and form new
- * splits.
- *
- * A split is a pair of containers that host activities in the same or different
- * processes, combined under the same parent window of the hosting task.
- *
- * A pair of activities can be put into a split by providing a static or runtime
- * split rule and then launching the activities in the same task using
- * [Activity.startActivity()][android.app.Activity.startActivity].
- */
+* A singleton controller class that gets information about the currently active activity
+* splits and provides interaction points to customize the splits and form new
+* splits.
+*
+* A split is a pair of containers that host activities in the same or different
+* processes, combined under the same parent window of the hosting task.
+*
+* A pair of activities can be put into a split by providing a static or runtime
+* split rule and then launching the activities in the same task using
+* [Activity.startActivity()][android.app.Activity.startActivity].
+*/
 class SplitController private constructor(applicationContext: Context) {
     private val embeddingBackend: EmbeddingBackend = ExtensionEmbeddingBackend
         .getInstance(applicationContext)
@@ -90,6 +90,42 @@
         return embeddingBackend.isSplitSupported()
     }
 
+    /**
+     * Sets or updates the previously registered [SplitAttributesCalculator].
+     *
+     * **Note** that if the [SplitAttributesCalculator] is replaced, the existing split pairs will
+     * be updated after there's a window or device state change.
+     * The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
+     *
+     * @param calculator the calculator to set. It will replace the previously set
+     * [SplitAttributesCalculator] if it exists.
+     * @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
+     * `false`
+     */
+    fun setSplitAttributesCalculator(calculator: SplitAttributesCalculator) {
+        embeddingBackend.setSplitAttributesCalculator(calculator)
+    }
+
+    /**
+     * Clears the previously set [SplitAttributesCalculator].
+     * The caller **must** make sure [isSplitAttributesCalculatorSupported] before invoking.
+     *
+     * @see androidx.window.embedding.SplitController.setSplitAttributesCalculator
+     * @throws UnsupportedOperationException if [isSplitAttributesCalculatorSupported] reports
+     * `false`
+     */
+    fun clearSplitAttributesCalculator() {
+        embeddingBackend.clearSplitAttributesCalculator()
+    }
+
+    /** Returns the current set [SplitAttributesCalculator]. */
+    fun getSplitAttributesCalculator(): SplitAttributesCalculator? =
+        embeddingBackend.getSplitAttributesCalculator()
+
+    /** Returns whether [SplitAttributesCalculator] is supported or not. */
+    fun isSplitAttributesCalculatorSupported(): Boolean =
+        embeddingBackend.isSplitAttributesCalculatorSupported()
+
     companion object {
         @Volatile
         private var globalInstance: SplitController? = null
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 3cabc9e..3bfea72 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
@@ -28,10 +28,8 @@
      * The [ActivityStack] representing the secondary split container.
      */
     val secondaryActivityStack: ActivityStack,
-    /**
-     * Ratio of the Task width that is given to the primary split container.
-     */
-    val splitRatio: Float
+    /** The [SplitAttributes] of this split pair. */
+    val splitAttributes: SplitAttributes
 ) {
     operator fun contains(activity: Activity): Boolean {
         return primaryActivityStack.contains(activity) ||
@@ -44,7 +42,7 @@
 
         if (primaryActivityStack != other.primaryActivityStack) return false
         if (secondaryActivityStack != other.secondaryActivityStack) return false
-        if (splitRatio != other.splitRatio) return false
+        if (splitAttributes != other.splitAttributes) return false
 
         return true
     }
@@ -52,7 +50,7 @@
     override fun hashCode(): Int {
         var result = primaryActivityStack.hashCode()
         result = 31 * result + secondaryActivityStack.hashCode()
-        result = 31 * result + splitRatio.hashCode()
+        result = 31 * result + splitAttributes.hashCode()
         return result
     }
 
@@ -61,7 +59,7 @@
             append("SplitInfo:{")
             append("primaryActivityStack=$primaryActivityStack,")
             append("secondaryActivityStack=$secondaryActivityStack,")
-            append("splitRatio=$splitRatio}")
+            append("splitAttributes=$splitAttributes}")
         }
     }
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index 1969f04..c3bf933 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -16,16 +16,15 @@
 
 package androidx.window.embedding
 
-import android.util.LayoutDirection.LOCALE
-import androidx.annotation.FloatRange
 import androidx.annotation.IntRange
-import androidx.core.util.Preconditions.checkArgument
 import androidx.core.util.Preconditions.checkArgumentNonnegative
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
 
 /**
  * Split configuration rules for activity pairs. Define when activities that were launched on top of
  * each other should be shown side-by-side, and the visual properties of such splits. Can be set
- * either via [RuleController.setRules] or via [RuleController.addRule]. The rules are always
+ * either by [RuleController.setRules] or [RuleController.addRule]. The rules are always
  * applied only to activities that will be started after the rules were set.
  */
 class SplitPairRule : SplitRule {
@@ -39,18 +38,16 @@
     /**
      * Determines what happens with the primary container when all activities are finished in the
      * associated secondary container.
-     * @see SplitRule.SplitFinishBehavior
+     * @see SplitRule.FinishBehavior
      */
-    @SplitFinishBehavior
-    val finishPrimaryWithSecondary: Int
+    val finishPrimaryWithSecondary: FinishBehavior
 
     /**
      * Determines what happens with the secondary container when all activities are finished in the
      * associated primary container.
-     * @see SplitRule.SplitFinishBehavior
+     * @see SplitRule.FinishBehavior
      */
-    @SplitFinishBehavior
-    val finishSecondaryWithPrimary: Int
+    val finishSecondaryWithPrimary: FinishBehavior
 
     /**
      * If there is an existing split with the same primary container, indicates whether the
@@ -60,18 +57,19 @@
     val clearTop: Boolean
 
     internal constructor(
+        tag: String? = null,
         filters: Set<SplitPairFilter>,
-        @SplitFinishBehavior finishPrimaryWithSecondary: Int = FINISH_NEVER,
-        @SplitFinishBehavior finishSecondaryWithPrimary: Int = FINISH_ALWAYS,
+        finishPrimaryWithSecondary: FinishBehavior = NEVER,
+        finishSecondaryWithPrimary: FinishBehavior = ALWAYS,
         clearTop: Boolean = false,
         @IntRange(from = 0) minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
+        @IntRange(from = 0) minHeightDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
         @IntRange(from = 0) minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
-        @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = 0.5f,
-        @LayoutDirection layoutDirection: Int = LOCALE
-    ) : super(minWidthDp, minSmallestWidthDp, splitRatio, layoutDirection) {
+        defaultSplitAttributes: SplitAttributes,
+    ) : super(tag, minWidthDp, minHeightDp, minSmallestWidthDp, defaultSplitAttributes) {
         checkArgumentNonnegative(minWidthDp, "minWidthDp must be non-negative")
+        checkArgumentNonnegative(minHeightDp, "minHeightDp must be non-negative")
         checkArgumentNonnegative(minSmallestWidthDp, "minSmallestWidthDp must be non-negative")
-        checkArgument(splitRatio in 0.0..1.0, "splitRatio must be in 0.0..1.0 range")
         this.filters = filters.toSet()
         this.clearTop = clearTop
         this.finishPrimaryWithSecondary = finishPrimaryWithSecondary
@@ -84,21 +82,19 @@
      * @param filters See [SplitPairRule.filters].
      */
     class Builder(
-        private val filters: Set<SplitPairFilter>,
+        private val filters: Set<SplitPairFilter>
     ) {
+        private var tag: String? = null
         @IntRange(from = 0)
         private var minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
         @IntRange(from = 0)
+        private var minHeightDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
+        @IntRange(from = 0)
         private var minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
-        @SplitFinishBehavior
-        private var finishPrimaryWithSecondary: Int = FINISH_NEVER
-        @SplitFinishBehavior
-        private var finishSecondaryWithPrimary: Int = FINISH_ALWAYS
+        private var finishPrimaryWithSecondary: FinishBehavior = NEVER
+        private var finishSecondaryWithPrimary: FinishBehavior = ALWAYS
         private var clearTop: Boolean = false
-        @FloatRange(from = 0.0, to = 1.0)
-        private var splitRatio: Float = 0.5f
-        @LayoutDirection
-        private var layoutDirection: Int = LOCALE
+        private var defaultSplitAttributes: SplitAttributes = SplitAttributes.Builder().build()
 
         /**
          * @see SplitPairRule.minWidthDp
@@ -107,6 +103,12 @@
             apply { this.minWidthDp = minWidthDp }
 
         /**
+         * @see SplitPairRule.minHeightDp
+         */
+        fun setMinHeightDp(@IntRange(from = 0) minHeightDp: Int): Builder =
+            apply { this.minHeightDp = minHeightDp }
+
+        /**
          * @see SplitPairRule.minSmallestWidthDp
          */
         fun setMinSmallestWidthDp(@IntRange(from = 0) minSmallestWidthDp: Int): Builder =
@@ -116,7 +118,7 @@
          * @see SplitPairRule.finishPrimaryWithSecondary
          */
         fun setFinishPrimaryWithSecondary(
-            @SplitFinishBehavior finishPrimaryWithSecondary: Int
+            finishPrimaryWithSecondary: FinishBehavior
         ): Builder =
             apply { this.finishPrimaryWithSecondary = finishPrimaryWithSecondary }
 
@@ -124,7 +126,7 @@
          * @see SplitPairRule.finishSecondaryWithPrimary
          */
         fun setFinishSecondaryWithPrimary(
-            @SplitFinishBehavior finishSecondaryWithPrimary: Int
+            finishSecondaryWithPrimary: FinishBehavior
         ): Builder =
             apply { this.finishSecondaryWithPrimary = finishSecondaryWithPrimary }
 
@@ -135,20 +137,25 @@
         fun setClearTop(clearTop: Boolean): Builder =
             apply { this.clearTop = clearTop }
 
-        /**
-         * @see SplitPairRule.splitRatio
-         */
-        fun setSplitRatio(@FloatRange(from = 0.0, to = 1.0) splitRatio: Float): Builder =
-            apply { this.splitRatio = splitRatio }
+        /** @see SplitPairRule.defaultSplitAttributes */
+        fun setDefaultSplitAttributes(defaultSplitAttributes: SplitAttributes): Builder =
+            apply { this.defaultSplitAttributes = defaultSplitAttributes }
 
-        /**
-         * @see SplitPairRule.layoutDirection
-         */
-        fun setLayoutDirection(@LayoutDirection layoutDirection: Int): Builder =
-            apply { this.layoutDirection = layoutDirection }
+        /** @see SplitPairRule.tag */
+        fun setTag(tag: String?): Builder =
+            apply { this.tag = tag }
 
-        fun build() = SplitPairRule(filters, finishPrimaryWithSecondary, finishSecondaryWithPrimary,
-            clearTop, minWidthDp, minSmallestWidthDp, splitRatio, layoutDirection)
+        fun build() = SplitPairRule(
+            tag,
+            filters,
+            finishPrimaryWithSecondary,
+            finishSecondaryWithPrimary,
+            clearTop,
+            minWidthDp,
+            minHeightDp,
+            minSmallestWidthDp,
+            defaultSplitAttributes,
+        )
     }
 
     /**
@@ -160,13 +167,14 @@
         newSet.addAll(filters)
         newSet.add(filter)
         return Builder(newSet.toSet())
+            .setTag(tag)
             .setMinWidthDp(minWidthDp)
+            .setMinHeightDp(minHeightDp)
             .setMinSmallestWidthDp(minSmallestWidthDp)
             .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
             .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
             .setClearTop(clearTop)
-            .setSplitRatio(splitRatio)
-            .setLayoutDirection(layoutDirection)
+            .setDefaultSplitAttributes(defaultSplitAttributes)
             .build()
     }
 
@@ -191,4 +199,17 @@
         result = 31 * result + clearTop.hashCode()
         return result
     }
+
+    override fun toString(): String =
+        "${SplitPairRule::class.java.simpleName}{" +
+            "tag=$tag" +
+            ", defaultSplitAttributes=$defaultSplitAttributes" +
+            ", minWidthDp=$minWidthDp" +
+            ", minHeightDp=$minHeightDp" +
+            ", minSmallestWidthDp=$minSmallestWidthDp" +
+            ", clearTop=$clearTop" +
+            ", finishPrimaryWithSecondary=$finishPrimaryWithSecondary" +
+            ", finishSecondaryWithPrimary=$finishSecondaryWithPrimary" +
+            ", filters=$filters" +
+            "}"
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
index 6250e82..d514d3e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -17,12 +17,11 @@
 package androidx.window.embedding
 
 import android.content.Intent
-import android.util.LayoutDirection.LOCALE
-import androidx.annotation.FloatRange
-import androidx.annotation.IntDef
 import androidx.annotation.IntRange
 import androidx.core.util.Preconditions.checkArgument
 import androidx.core.util.Preconditions.checkArgumentNonnegative
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ALWAYS
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.NEVER
 
 /**
  * Configuration rules for split placeholders.
@@ -60,37 +59,30 @@
     val isSticky: Boolean
 
     /**
-     * Defines whether a container should be finished together when the associated placeholder
-     * activity is being finished based on current presentation mode.
-     */
-    @Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(FINISH_ALWAYS, FINISH_ADJACENT)
-    internal annotation class SplitPlaceholderFinishBehavior
-
-    /**
      * Determines what happens with the primary container when all activities are finished in the
      * associated placeholder container.
-     * @see SplitPlaceholderFinishBehavior
+     *
+     * **Note** that it is not valid to set [SplitRule.FinishBehavior.NEVER]
+     * @see SplitRule.FinishBehavior
      */
-    @SplitPlaceholderFinishBehavior
-    val finishPrimaryWithPlaceholder: Int
+    val finishPrimaryWithPlaceholder: FinishBehavior
 
     internal constructor(
+        tag: String? = null,
         filters: Set<ActivityFilter>,
         placeholderIntent: Intent,
         isSticky: Boolean,
-        @SplitPlaceholderFinishBehavior finishPrimaryWithPlaceholder: Int = FINISH_ALWAYS,
+        finishPrimaryWithPlaceholder: FinishBehavior = ALWAYS,
         @IntRange(from = 0) minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
+        @IntRange(from = 0) minHeightDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
         @IntRange(from = 0) minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
-        @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = 0.5f,
-        @LayoutDirection layoutDirection: Int = LOCALE
-    ) : super(minWidthDp, minSmallestWidthDp, splitRatio, layoutDirection) {
+        defaultSplitAttributes: SplitAttributes,
+    ) : super(tag, minWidthDp, minHeightDp, minSmallestWidthDp, defaultSplitAttributes) {
         checkArgumentNonnegative(minWidthDp, "minWidthDp must be non-negative")
+        checkArgumentNonnegative(minHeightDp, "minHeightDp must be non-negative")
         checkArgumentNonnegative(minSmallestWidthDp, "minSmallestWidthDp must be non-negative")
-        checkArgument(splitRatio in 0.0..1.0, "splitRatio must be in 0.0..1.0 range")
-        checkArgument(finishPrimaryWithPlaceholder != FINISH_NEVER,
-            "FINISH_NEVER is not a valid configuration for SplitPlaceholderRule. " +
+        checkArgument(finishPrimaryWithPlaceholder != NEVER,
+            "NEVER is not a valid configuration for SplitPlaceholderRule. " +
                 "Please use FINISH_ALWAYS or FINISH_ADJACENT instead or refer to the current API.")
         this.filters = filters.toSet()
         this.placeholderIntent = placeholderIntent
@@ -100,25 +92,23 @@
 
     /**
      * Builder for [SplitPlaceholderRule].
-     *
      * @param filters See [SplitPlaceholderRule.filters].
      * @param placeholderIntent See [SplitPlaceholderRule.placeholderIntent].
      */
     class Builder(
         private val filters: Set<ActivityFilter>,
-        private val placeholderIntent: Intent,
+        private val placeholderIntent: Intent
     ) {
         @IntRange(from = 0)
         private var minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
         @IntRange(from = 0)
+        private var minHeightDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
+        @IntRange(from = 0)
         private var minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
-        @SplitPlaceholderFinishBehavior
-        private var finishPrimaryWithPlaceholder: Int = FINISH_ALWAYS
+        private var finishPrimaryWithPlaceholder: FinishBehavior = ALWAYS
         private var isSticky: Boolean = false
-        @FloatRange(from = 0.0, to = 1.0)
-        private var splitRatio: Float = 0.5f
-        @LayoutDirection
-        private var layoutDirection: Int = LOCALE
+        private var defaultSplitAttributes: SplitAttributes = SplitAttributes.Builder().build()
+        private var tag: String? = null
 
         /**
          * @see SplitPlaceholderRule.minWidthDp
@@ -127,6 +117,12 @@
             apply { this.minWidthDp = minWidthDp }
 
         /**
+         * @see SplitPlaceholderRule.minHeightDp
+         */
+        fun setMinHeightDp(@IntRange(from = 0) minHeightDp: Int): Builder =
+            apply { this.minHeightDp = minHeightDp }
+
+        /**
          * @see SplitPlaceholderRule.minSmallestWidthDp
          */
         fun setMinSmallestWidthDp(@IntRange(from = 0) minSmallestWidthDp: Int): Builder =
@@ -135,9 +131,7 @@
         /**
          * @see SplitPlaceholderRule.finishPrimaryWithPlaceholder
          */
-        fun setFinishPrimaryWithPlaceholder(
-            @SplitPlaceholderFinishBehavior finishPrimaryWithPlaceholder: Int
-        ): Builder =
+        fun setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder: FinishBehavior): Builder =
             apply {
                this.finishPrimaryWithPlaceholder = finishPrimaryWithPlaceholder
             }
@@ -148,21 +142,25 @@
         fun setSticky(isSticky: Boolean): Builder =
             apply { this.isSticky = isSticky }
 
-        /**
-         * @see SplitPlaceholderRule.splitRatio
-         */
-        fun setSplitRatio(@FloatRange(from = 0.0, to = 1.0) splitRatio: Float): Builder =
-            apply { this.splitRatio = splitRatio }
+        /** @see SplitPlaceholderRule.defaultSplitAttributes */
+        fun setDefaultSplitAttributes(defaultSplitAttributes: SplitAttributes): Builder =
+            apply { this.defaultSplitAttributes = defaultSplitAttributes }
 
-        /**
-         * @see SplitPlaceholderRule.layoutDirection
-         */
-        fun setLayoutDirection(@LayoutDirection layoutDirection: Int): Builder =
-            apply { this.layoutDirection = layoutDirection }
+        /** @see SplitPlaceholderRule.tag */
+        fun setTag(tag: String?): Builder =
+            apply { this.tag = tag }
 
-        fun build() = SplitPlaceholderRule(filters, placeholderIntent, isSticky,
-            finishPrimaryWithPlaceholder, minWidthDp, minSmallestWidthDp, splitRatio,
-            layoutDirection)
+        fun build() = SplitPlaceholderRule(
+            tag,
+            filters,
+            placeholderIntent,
+            isSticky,
+            finishPrimaryWithPlaceholder,
+            minWidthDp,
+            minHeightDp,
+            minSmallestWidthDp,
+            defaultSplitAttributes,
+        )
     }
 
     /**
@@ -174,12 +172,13 @@
         newSet.addAll(filters)
         newSet.add(filter)
         return Builder(newSet.toSet(), placeholderIntent)
+            .setTag(tag)
             .setMinWidthDp(minWidthDp)
+            .setMinHeightDp(minHeightDp)
             .setMinSmallestWidthDp(minSmallestWidthDp)
             .setSticky(isSticky)
             .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
-            .setSplitRatio(splitRatio)
-            .setLayoutDirection(layoutDirection)
+            .setDefaultSplitAttributes(defaultSplitAttributes)
             .build()
     }
 
@@ -204,4 +203,17 @@
         result = 31 * result + filters.hashCode()
         return result
     }
+
+    override fun toString(): String =
+         "SplitPlaceholderRule{" +
+             "tag=$tag" +
+             ", defaultSplitAttributes=$defaultSplitAttributes" +
+             ", minWidthDp=$minWidthDp" +
+             ", minHeightDp=$minHeightDp" +
+             ", minSmallestWidthDp=$minSmallestWidthDp" +
+             ", placeholderIntent=$placeholderIntent" +
+             ", isSticky=$isSticky" +
+             ", finishPrimaryWithPlaceholder=$finishPrimaryWithPlaceholder" +
+             ", filters=$filters" +
+             "}"
 }
\ No newline at end of file
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 eb773d3..db6af5c 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -19,16 +19,12 @@
 import android.content.Context
 import android.graphics.Rect
 import android.os.Build
-import android.util.LayoutDirection.LOCALE
-import android.util.LayoutDirection.LTR
-import android.util.LayoutDirection.RTL
 import android.view.WindowMetrics
 import androidx.annotation.DoNotInline
-import androidx.annotation.FloatRange
-import androidx.annotation.IntDef
 import androidx.annotation.IntRange
 import androidx.annotation.RequiresApi
 import androidx.window.embedding.SplitRule.Companion.DEFAULT_SPLIT_MIN_DIMENSION_DP
+import androidx.window.embedding.SplitRule.FinishBehavior.Companion.ADJACENT
 import kotlin.math.min
 
 /**
@@ -37,12 +33,28 @@
  * via [RuleController.addRule]. The rules are always applied only to activities that will be
  * started after the rules were set.
  *
+ * Note that regardless of whether the minimal requirements ([minWidthDp], [minHeightDp] and
+ * [minSmallestWidthDp]) are met or not, [SplitAttributesCalculator.computeSplitAttributesForParams]
+ * will still be called for the rule if the calculator is registered via
+ * [SplitController.setSplitAttributesCalculator]. Whether this [SplitRule]'s
+ * minimum requirements are satisfied is dispatched in
+ * [SplitAttributesCalculator.SplitAttributesCalculatorParams.isDefaultMinSizeSatisfied] instead.
+ * The width and height could be verified in
+ * [SplitAttributesCalculator.computeSplitAttributesForParams] as the sample linked below shows.
+ * It is useful if this rule is supported to split the parent container in different directions
+ * with different device states.
+ *
+ * It is useful if this [SplitRule] is supported to split the parent container in different
+ * directions with different device states.
+ *
+ * @sample androidx.window.samples.embedding.splitWithOrientations
  * @see androidx.window.embedding.SplitPairRule
  * @see androidx.window.embedding.SplitPlaceholderRule
  */
 open class SplitRule internal constructor(
+    tag: String? = null,
     /**
-     * The smallest value of width of the parent window when the split should be used, in DP.
+     * The smallest value of width 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.
      *
@@ -53,73 +65,41 @@
     val minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
 
     /**
-     * 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 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 [SplitRule].
+     *
+     * The default is [DEFAULT_SPLIT_MIN_DIMENSION_DP] if the app doesn't set.
+     * `0` means to always allow split.
+     *
+     * @see SplitAttributes.LayoutDirection.TOP_TO_BOTTOM
+     * @see SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
+     */
+    @IntRange(from = 0)
+    val minHeightDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
+
+    /**
+     * The smallest value of the smallest possible width of the parent task 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 [DEFAULT_SPLIT_MIN_DIMENSION_DP] if the app doesn't set.
      * `0` means to always allow split.
      */
     @IntRange(from = 0)
-    val minSmallestWidthDp: Int,
+    val minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
 
     /**
-     * Defines what part of the width should be given to the primary activity. Defaults to an
-     * equal width split.
+     * The default [SplitAttributes] to apply on the activity containers pair when the host task
+     * bounds satisfy [minWidthDp], [minHeightDp] and [minSmallestWidthDp] requirements.
      */
-    @FloatRange(from = 0.0, to = 1.0)
-    val splitRatio: Float = 0.5f,
+    val defaultSplitAttributes: SplitAttributes,
+) : EmbeddingRule(tag) {
 
-    /**
-     * The layout direction for the split. The value must be one of [LTR], [RTL] or [LOCALE].
-     * - [LTR]: It splits the task bounds vertically, and put the primary container on the left
-     *   portion, and the secondary container on the right portion.
-     * - [RTL]: It splits the task bounds vertically, and put the primary container on the right
-     *   portion, and the secondary container on the left portion.
-     * - [LOCALE]: It splits the task bounds vertically, and the direction is deduced from the
-     *   default language script of locale. The direction can be either [LTR] or [RTL].
-     */
-    @LayoutDirection
-    val layoutDirection: Int = LOCALE
-) : EmbeddingRule() {
-
-    @IntDef(LTR, RTL, LOCALE)
-    @Retention(AnnotationRetention.SOURCE)
-    internal annotation class LayoutDirection
-
-    /**
-     * Determines what happens with the associated container when all activities are finished in
-     * one of the containers in a split.
-     *
-     * For example, given that [SplitPairRule.finishPrimaryWithSecondary] is [FINISH_ADJACENT] and
-     * secondary container finishes. The primary associated container is finished if it's
-     * side-by-side with secondary container. The primary associated container is not finished
-     * if it occupies entire task bounds.
-     *
-     * @see SplitPairRule.finishPrimaryWithSecondary
-     * @see SplitPairRule.finishSecondaryWithPrimary
-     * @see SplitPlaceholderRule.finishPrimaryWithPlaceholder
-     */
     companion object {
         /**
-         * Never finish the associated container.
-         * @see SplitRule.Companion
-         */
-        const val FINISH_NEVER = 0
-        /**
-         * Always finish the associated container independent of the current presentation mode.
-         * @see SplitRule.Companion
-         */
-        const val FINISH_ALWAYS = 1
-        /**
-         * Only finish the associated container when displayed side-by-side/adjacent to the one
-         * being finished. Does not finish the associated one when containers are stacked on top of
-         * each other.
-         * @see SplitRule.Companion
-         */
-        const val FINISH_ADJACENT = 2
-        /**
          * The default min dimension in DP for allowing split if it is not set by apps. The value
          * reflects [androidx.window.core.layout.WindowWidthSizeClass.MEDIUM].
          */
@@ -127,12 +107,55 @@
     }
 
     /**
-     * Defines whether an associated container should be finished together with the one that's
-     * already being finished based on their current presentation mode.
+     * Determines what happens with the associated container when all activities are finished in
+     * one of the containers in a split.
+     *
+     * For example, given that [SplitPairRule.finishPrimaryWithSecondary] is [ADJACENT] and
+     * secondary container finishes. The primary associated container is finished if it's
+     * side-by-side with secondary container. The primary associated container is not finished
+     * if it occupies entire task bounds.
+     *
+     * @see SplitPairRule.finishPrimaryWithSecondary
+     * @see SplitPairRule.finishSecondaryWithPrimary
+     * @see SplitPlaceholderRule.finishPrimaryWithPlaceholder
      */
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(FINISH_NEVER, FINISH_ALWAYS, FINISH_ADJACENT)
-    internal annotation class SplitFinishBehavior
+    class FinishBehavior private constructor(
+        /** The description of this [FinishBehavior] */
+        private val description: String,
+        /** The enum value defined in `splitLayoutDirection` attributes in `attrs.xml` */
+        internal val value: Int,
+    ) {
+        override fun toString(): String = description
+
+        companion object {
+            /** Never finish the associated container. */
+            @JvmField
+            val NEVER = FinishBehavior("NEVER", 0)
+            /**
+             * Always finish the associated container independent of the current presentation mode.
+             */
+            @JvmField
+            val ALWAYS = FinishBehavior("ALWAYS", 1)
+            /**
+             * Only finish the associated container when displayed side-by-side/adjacent to the one
+             * being finished. Does not finish the associated one when containers are stacked on top
+             * of each other.
+             */
+            @JvmField
+            val ADJACENT = FinishBehavior("ADJACENT", 2)
+
+            @JvmStatic
+            internal fun getFinishBehaviorFromValue(
+                @IntRange(from = 0, to = 2) value: Int
+            ): FinishBehavior =
+                when (value) {
+                    NEVER.value -> NEVER
+                    ALWAYS.value -> ALWAYS
+                    ADJACENT.value -> ADJACENT
+                    else -> throw IllegalArgumentException("Unknown finish behavior:$value")
+                }
+        }
+    }
 
     /**
      * Verifies if the provided parent bounds are large enough to apply the rule.
@@ -153,13 +176,13 @@
      */
     internal fun checkParentBounds(density: Float, bounds: Rect): Boolean {
         val minWidthPx = convertDpToPx(density, minWidthDp)
+        val minHeightPx = convertDpToPx(density, minHeightDp)
         val minSmallestWidthPx = convertDpToPx(density, minSmallestWidthDp)
-        val validMinWidth = (minWidthDp == 0 || bounds.width() >= minWidthPx)
-        val validSmallestMinWidth = (
-            minSmallestWidthDp == 0 ||
-                min(bounds.width(), bounds.height()) >= minSmallestWidthPx
-            )
-        return validMinWidth && validSmallestMinWidth
+        val validMinWidth = minWidthDp == 0 || bounds.width() >= minWidthPx
+        val validMinHeight = minHeightDp == 0 || bounds.height() >= minHeightPx
+        val validSmallestMinWidth =
+            minSmallestWidthDp == 0 || min(bounds.width(), bounds.height()) >= minSmallestWidthPx
+        return validMinWidth && validMinHeight && validSmallestMinWidth
     }
 
     /**
@@ -181,19 +204,20 @@
         if (this === other) return true
         if (other !is SplitRule) return false
 
+        if (!super.equals(other)) return false
         if (minWidthDp != other.minWidthDp) return false
+        if (minHeightDp != other.minHeightDp) return false
         if (minSmallestWidthDp != other.minSmallestWidthDp) return false
-        if (splitRatio != other.splitRatio) return false
-        if (layoutDirection != other.layoutDirection) return false
-
+        if (defaultSplitAttributes != other.defaultSplitAttributes) return false
         return true
     }
 
     override fun hashCode(): Int {
-        var result = minWidthDp
+        var result = super.hashCode()
+        result = 31 * result + minWidthDp
+        result = 31 * result + minHeightDp
         result = 31 * result + minSmallestWidthDp
-        result = 31 * result + splitRatio.hashCode()
-        result = 31 * result + layoutDirection
+        result = 31 * result + defaultSplitAttributes.hashCode()
         return result
     }
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/layout/ContextCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/ContextCompatHelper.kt
new file mode 100644
index 0000000..2bc997d
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/ContextCompatHelper.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 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.window.layout.util
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Rect
+import android.os.Build
+import android.view.WindowManager
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
+import androidx.core.view.WindowInsetsCompat
+import androidx.window.layout.WindowMetrics
+
+@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 currentWindowInsets(@UiContext context: Context): WindowInsetsCompat {
+        val wm = context.getSystemService(WindowManager::class.java)
+        return WindowInsetsCompat.toWindowInsetsCompat(wm.currentWindowMetrics.windowInsets)
+    }
+
+    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
+     * inlined.
+     *
+     * @see androidx.window.layout.WindowMetrics.getWindowInsets
+     */
+    @DoNotInline
+    fun currentWindowInsets(activity: Activity): WindowInsetsCompat {
+        val platformInsets = activity.windowManager.currentWindowMetrics.windowInsets
+        return WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
+    }
+}
\ No newline at end of file
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 3ae9ec8..1816538 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTracker.kt
@@ -18,9 +18,11 @@
 
 import android.app.Activity
 import android.content.Context
+import android.inputmethodservice.InputMethodService
 import android.util.Log
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
+import androidx.annotation.UiContext
 import androidx.window.core.ConsumerAdapter
 import androidx.window.layout.adapter.WindowBackend
 import androidx.window.layout.adapter.extensions.ExtensionWindowLayoutInfoBackend
@@ -37,6 +39,33 @@
      * A [Flow] of [WindowLayoutInfo] that contains all the available features. A [WindowLayoutInfo]
      * contains a [List] of [DisplayFeature] that intersect the associated [android.view.Window].
      *
+     * This method exports the same content as
+     * [WindowLayoutInfo.windowLayoutInfo(activity: Activity)], but also supports non-Activity
+     * windows to receive [WindowLayoutInfo] updates.
+     *
+     * A derived class may throw NotImplementedError if this method is not overridden.
+     * Obtaining a [WindowInfoTracker] through [WindowInfoTracker.getOrCreate] guarantees having a
+     * default implementation for this method.
+     *
+     * @param context a [UiContext] such as an [Activity], an [InputMethodService], or an instance
+     * created via [Context.createWindowContext] that listens to configuration changes.
+     * @see WindowLayoutInfo
+     * @see DisplayFeature
+     *
+     * @throws NotImplementedError when [Context] is not an [UiContext] or this method has no
+     * supporting implementation.
+     */
+    fun windowLayoutInfo(@UiContext context: Context): Flow<WindowLayoutInfo> {
+        val windowLayoutInfoFlow: Flow<WindowLayoutInfo>? = windowLayoutInfo((context as Activity))
+        return windowLayoutInfoFlow
+            ?: throw NotImplementedError(
+                message = "Must override windowLayoutInfo(context) and provide an implementation.")
+    }
+
+    /**
+     * A [Flow] of [WindowLayoutInfo] that contains all the available features. A [WindowLayoutInfo]
+     * contains a [List] of [DisplayFeature] that intersect the associated [android.view.Window].
+     *
      * The first [WindowLayoutInfo] will not be emitted until [Activity.onStart] has been called.
      * which values you receive and when is device dependent. It is recommended to test scenarios
      * where there is a long delay between subscribing and receiving the first value or never
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 16efaad..0796437 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoTrackerImpl.kt
@@ -18,6 +18,7 @@
 
 import android.app.Activity
 import android.content.Context
+import androidx.annotation.UiContext
 import androidx.core.util.Consumer
 import androidx.window.layout.adapter.WindowBackend
 import kotlinx.coroutines.channels.awaitClose
@@ -26,7 +27,7 @@
 
 /**
  * An implementation of [WindowInfoTracker] that provides the [WindowLayoutInfo] and
- * [WindowMetrics] for the given [Activity].
+ * [WindowMetrics] for the given [Activity] or [UiContext].
  *
  * @param windowMetricsCalculator a helper to calculate the [WindowMetrics] for the [Activity].
  * @param windowBackend a helper to provide the [WindowLayoutInfo].
@@ -37,9 +38,21 @@
 ) : WindowInfoTracker {
 
     /**
-     * A [Flow] of window layout changes in the current visual [Context].
-     *
-     * @see Activity.onAttachedToWindow
+     * A [Flow] of window layout changes in the current visual [UiContext]. A context has to be
+     * either an [Activity] or created with [Context#createWindowContext].
+     */
+    override fun windowLayoutInfo(@UiContext context: Context): Flow<WindowLayoutInfo> {
+        return callbackFlow {
+            val listener = Consumer { info: WindowLayoutInfo -> trySend(info) }
+            windowBackend.registerLayoutChangeCallback(context, Runnable::run, listener)
+            awaitClose {
+                windowBackend.unregisterLayoutChangeCallback(listener)
+            }
+        }
+    }
+
+    /**
+     * A [Flow] of window layout changes in the current visual [Activity].
      */
     override fun windowLayoutInfo(activity: Activity): Flow<WindowLayoutInfo> {
         return callbackFlow {
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 bbf6d69..449c566 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetrics.kt
@@ -19,7 +19,6 @@
 import android.os.Build.VERSION_CODES
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
-import androidx.annotation.RestrictTo.Scope.TESTS
 import androidx.core.view.WindowInsetsCompat
 import androidx.window.core.Bounds
 import androidx.window.core.ExperimentalWindowApi
@@ -42,7 +41,7 @@
      * An internal constructor for [WindowMetrics]
      * @suppress
      */
-    @RestrictTo(TESTS)
+    @RestrictTo(RestrictTo.Scope.TESTS)
     constructor(
         bounds: Rect,
         insets: WindowInsetsCompat = WindowInsetsCompat.Builder().build()
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 0de1501..f7181bb 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculator.kt
@@ -16,14 +16,20 @@
 
 package androidx.window.layout
 
+import android.view.WindowMetrics as AndroidWindowMetrics
 import android.app.Activity
+import android.content.Context
+import android.inputmethodservice.InputMethodService
 import android.os.Build
 import android.view.Display
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
+import androidx.annotation.UiContext
+import androidx.core.view.WindowInsetsCompat
 import androidx.window.core.ExperimentalWindowApi
 
 /**
- * An interface to calculate the [WindowMetrics] for an [Activity].
+ * An interface to calculate the [WindowMetrics] for an [Activity] or a [UiContext].
  */
 interface WindowMetricsCalculator {
 
@@ -65,6 +71,24 @@
     fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics
 
     /**
+     * Computes the size and position of the area the window would occupy with
+     * [MATCH_PARENT][android.view.WindowManager.LayoutParams.MATCH_PARENT] width and height
+     * and any combination of flags that would allow the window to extend behind display cutouts.
+     *
+     * On [Build.VERSION_CODES.Q] and older, a [UiContext] is either an [Activity] or an
+     * [InputMethodService]. On [Build.VERSION_CODES.R] and newer, a [UiContext] can also be one
+     * created via the [Context.createWindowContext] APIs.
+     *
+     * @see [computeCurrentWindowMetrics]
+     * @throws NotImplementedError if not implemented. The default implementation from [getOrCreate]
+     * is guaranteed to implement this method.
+     */
+    fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
+        throw NotImplementedError("Must override computeCurrentWindowMetrics(context) and" +
+            " provide an implementation.")
+    }
+
+    /**
      * Computes the maximum size and position of the area the window can expect with
      * [MATCH_PARENT][android.view.WindowManager.LayoutParams.MATCH_PARENT] width and height
      * and any combination of flags that would allow the window to extend behind display cutouts.
@@ -76,6 +100,27 @@
      */
     fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics
 
+    /**
+     * Computes the maximum size and position of the area the window can expect with
+     * [MATCH_PARENT][android.view.WindowManager.LayoutParams.MATCH_PARENT] width and height
+     * and any combination of flags that would allow the window to extend behind display cutouts.
+     *
+     * The value returned from this method will always match [Display.getRealSize] on
+     * [Android 10][Build.VERSION_CODES.Q] and below.
+     *
+     * On [Build.VERSION_CODES.Q] and older, a [UiContext] is either an [Activity] or an
+     * [InputMethodService]. On [Build.VERSION_CODES.R] and newer, a [UiContext] can also be one
+     * created via the [Context.createWindowContext] APIs.
+     *
+     * @see [computeMaximumWindowMetrics]
+     * @throws NotImplementedError if not implemented. The default implementation from [getOrCreate]
+     * is guaranteed to implement this method.
+     */
+    fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
+        throw NotImplementedError("Must override computeMaximumWindowMetrics(context) and" +
+            " provide an implementation.")
+    }
+
     companion object {
 
         private var decorator: (WindowMetricsCalculator) -> WindowMetricsCalculator =
@@ -99,6 +144,18 @@
         fun reset() {
             decorator = { it }
         }
+
+        /**
+         * 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)
+            )
     }
 }
 
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 4fa733a..72e8ffa 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowMetricsCalculatorCompat.kt
@@ -21,19 +21,24 @@
 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.ActivityCompatHelperApi30.currentWindowBounds
-import androidx.window.layout.util.ActivityCompatHelperApi30.currentWindowInsets
-import androidx.window.layout.util.ActivityCompatHelperApi30.maximumWindowBounds
+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.ContextUtils.unwrapUiContext
 import androidx.window.layout.util.DisplayCompatHelperApi17.getRealSize
 import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetBottom
 import androidx.window.layout.util.DisplayCompatHelperApi28.safeInsetLeft
@@ -49,6 +54,42 @@
     private val TAG: String = WindowMetricsCalculatorCompat::class.java.simpleName
 
     /**
+     * Computes the current [WindowMetrics] for a given [Context]. The context can be either
+     * an [Activity], a Context created with [Context#createWindowContext], or an
+     * [InputMethodService].
+     * @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 (unwrapUiContext(context)) {
+                is Activity -> {
+                    return computeCurrentWindowMetrics(context as Activity)
+                }
+                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")
+                }
+            }
+        }
+    }
+
+    /**
      * Computes the current [WindowMetrics] for a given [Activity]
      * @see WindowMetricsCalculator.computeCurrentWindowMetrics
      */
@@ -78,19 +119,30 @@
      * @see WindowMetricsCalculator.computeMaximumWindowMetrics
      */
     override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
+        return computeMaximumWindowMetrics(activity as Context)
+    }
+
+    /**
+     * Computes the maximum [WindowMetrics] for a given [UiContext]
+     * @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(activity)
+            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
+            // compatibility with older versions, as we can't reliably get the display associated
+            // with a Context through public APIs either.
             @Suppress("DEPRECATION")
-            val display = activity.windowManager.defaultDisplay
+            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(activity)
+            computeWindowInsetsCompat(context)
         } else {
             WindowInsetsCompat.Builder().build()
         }
@@ -408,13 +460,13 @@
     )
 
     /**
-     * Computes the current [WindowInsetsCompat] for a given [Activity].
+     * Computes the current [WindowInsetsCompat] for a given [Context].
      */
     @RequiresApi(VERSION_CODES.R)
-    internal fun computeWindowInsetsCompat(activity: Activity): WindowInsetsCompat {
+    internal fun computeWindowInsetsCompat(@UiContext context: Context): WindowInsetsCompat {
         val build = Build.VERSION.SDK_INT
         val windowInsetsCompat = if (build >= VERSION_CODES.R) {
-            currentWindowInsets(activity)
+            currentWindowInsets(context)
         } else {
             throw Exception("Incompatible SDK version")
         }
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 d799f4a..3524cb1 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
@@ -16,27 +16,32 @@
 package androidx.window.layout.adapter
 
 import android.app.Activity
+import android.content.Context
 import androidx.core.util.Consumer
 import androidx.window.layout.WindowLayoutInfo
 import java.util.concurrent.Executor
+import androidx.annotation.UiContext
 
 /**
  * Backing interface for [androidx.window.layout.WindowInfoTracker] instances that serve as the
  * default information supplier.
  */
 internal interface WindowBackend {
+
     /**
-     * Registers a callback for layout changes of the window for the supplied [Activity].
+     * Registers a callback for layout changes of the window for the supplied [UiContext].
      * Must be called only after the it is attached to the window.
+     * The supplied [UiContext] should correspond to a window or an area on the screen. It must be
+     * either an [Activity] or a [UiContext] created with [Context#createWindowContext].
      */
     fun registerLayoutChangeCallback(
-        activity: Activity,
+        @UiContext context: Context,
         executor: Executor,
         callback: Consumer<WindowLayoutInfo>
     )
 
     /**
-     * Unregisters a callback for window layout changes of the [Activity] window.
+     * Unregisters a callback for window layout changes.
      */
     fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>)
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt
index 2c304b4..630d8d5 100644
--- a/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt
+++ b/window/window/src/main/java/androidx/window/layout/adapter/extensions/ExtensionWindowLayoutInfoBackend.kt
@@ -18,10 +18,13 @@
 
 import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
 import android.app.Activity
+import android.content.Context
 import androidx.annotation.GuardedBy
+import androidx.annotation.UiContext
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.Consumer
 import androidx.window.core.ConsumerAdapter
+import androidx.window.core.ExtensionsUtil
 import androidx.window.extensions.layout.WindowLayoutComponent
 import androidx.window.layout.WindowLayoutInfo
 import androidx.window.layout.adapter.WindowBackend
@@ -32,8 +35,9 @@
 
 /**
  * A wrapper around [WindowLayoutComponent] that ensures
- * [WindowLayoutComponent.addWindowLayoutInfoListener] is called at most once per activity while
- * there are active listeners.
+ * [WindowLayoutComponent.addWindowLayoutInfoListener] is called at most once per context while
+ * there are active listeners. Context has to be an [Activity] or a [UiContext] created with
+ * [Context#createWindowContext] or InputMethodService.
  */
 internal class ExtensionWindowLayoutInfoBackend(
     private val component: WindowLayoutComponent,
@@ -42,72 +46,107 @@
 
     private val extensionWindowBackendLock = ReentrantLock()
     @GuardedBy("lock")
-    private val activityToListeners = mutableMapOf<Activity, MulticastConsumer>()
+    private val contextToListeners = mutableMapOf<Context, MulticastConsumer>()
+
     @GuardedBy("lock")
-    private val listenerToActivity = mutableMapOf<Consumer<WindowLayoutInfo>, Activity>()
+    private val listenerToContext = mutableMapOf<Consumer<WindowLayoutInfo>, Context>()
+
     @GuardedBy("lock")
     private val consumerToToken = mutableMapOf<MulticastConsumer, ConsumerAdapter.Subscription>()
 
     /**
      * Registers a listener to consume new values of [WindowLayoutInfo]. If there was a listener
-     * registered for a given [Activity] then the new listener will receive a replay of the last
+     * registered for a given [Context] then the new listener will receive a replay of the last
      * known value.
-     * @param activity the host of a [android.view.Window]
+     * @param context the host of a [android.view.Window] or an area on the screen. Has to be an
+     * [Activity] or a [UiContext] created with [Context#createWindowContext] or InputMethodService.
      * @param executor an executor from the parent interface
      * @param callback the listener that will receive new values
      */
+    @OptIn(androidx.window.core.ExperimentalWindowApi::class)
     override fun registerLayoutChangeCallback(
-        activity: Activity,
+        @UiContext context: Context,
         executor: Executor,
         callback: Consumer<WindowLayoutInfo>
     ) {
         extensionWindowBackendLock.withLock {
-            activityToListeners[activity]?.let { listener ->
+            contextToListeners[context]?.let { listener ->
                 listener.addListener(callback)
-                listenerToActivity[callback] = activity
+                listenerToContext[callback] = context
             } ?: run {
-                val consumer = MulticastConsumer(activity)
-                activityToListeners[activity] = consumer
-                listenerToActivity[callback] = activity
+                val consumer = MulticastConsumer(context)
+                contextToListeners[context] = consumer
+                listenerToContext[callback] = context
                 consumer.addListener(callback)
-                val disposableToken = consumerAdapter.createSubscription(
-                    component,
-                    OEMWindowLayoutInfo::class,
-                    "addWindowLayoutInfoListener",
-                    "removeWindowLayoutInfoListener",
-                    activity
-                ) { value ->
-                    consumer.accept(value)
+                val consumeWindowLayoutInfo: (OEMWindowLayoutInfo) -> Unit = {
+                        value -> consumer.accept(value)
                 }
+                // The registrations above maintain 1-many mapping of Context-Listeners across
+                // different subscription implementations.
+                val disposableToken =
+                    when (ExtensionsUtil.safeVendorApiLevel) {
+                        // TODO(b/246640575) Use Extension Level constants here instead of raw ints.
+                        2 ->
+                            consumerAdapter.createSubscription(
+                                component,
+                                OEMWindowLayoutInfo::class,
+                                "addWindowLayoutInfoListener",
+                                "removeWindowLayoutInfoListener",
+                                context,
+                                consumeWindowLayoutInfo
+                            )
+                        1 ->
+                            if (context is Activity) {
+                                consumerAdapter.createSubscription(
+                                    component,
+                                    OEMWindowLayoutInfo::class,
+                                    "addWindowLayoutInfoListener",
+                                    "removeWindowLayoutInfoListener",
+                                    context,
+                                    consumeWindowLayoutInfo
+                                )
+                            } else {
+                                // Prior to WM Extensions v2 addWindowLayoutInfoListener only
+                                // supports Activities. Return empty WindowLayoutInfo if the
+                                // provided Context is not an Activity.
+                                consumer.accept(OEMWindowLayoutInfo(emptyList()))
+                                return@registerLayoutChangeCallback
+                            }
+                        else -> {
+                            consumer.accept(OEMWindowLayoutInfo(emptyList()))
+                            return@registerLayoutChangeCallback
+                        }
+                    }
                 consumerToToken[consumer] = disposableToken
             }
         }
     }
 
     /**
-     * Unregisters a listener, if this is the last listener for an [Activity] then the listener is
+     * Unregisters a listener, if this is the last listener for a [UiContext] then the listener is
      * removed from the [WindowLayoutComponent]. Calling with the same listener multiple times in a
-     * row does not have an effect. @param callback a listener that may have been registered
+     * row does not have an effect.
+     * @param callback a listener that may have been registered
      */
     override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
         extensionWindowBackendLock.withLock {
-            val activity = listenerToActivity[callback] ?: return
-            val multicastListener = activityToListeners[activity] ?: return
+            val context = listenerToContext[callback] ?: return
+            val multicastListener = contextToListeners[context] ?: return
             multicastListener.removeListener(callback)
-            listenerToActivity.remove(callback)
+            listenerToContext.remove(callback)
             if (multicastListener.isEmpty()) {
                 consumerToToken.remove(multicastListener)?.dispose()
-                activityToListeners.remove(activity)
+                contextToListeners.remove(context)
             }
         }
     }
 
     /**
-     * Returns {@code true} there is any registered listener information, {@code false} otherwise.
+     * Returns {@code true} if all the collections are empty, {@code false} otherwise
      */
     @VisibleForTesting
     fun hasRegisteredListeners(): Boolean {
-        return !(activityToListeners.isEmpty() && listenerToActivity.isEmpty() &&
+        return !(contextToListeners.isEmpty() && listenerToContext.isEmpty() &&
             consumerToToken.isEmpty())
     }
 
@@ -117,7 +156,7 @@
      * value whenever a new consumer registers.
      */
     private class MulticastConsumer(
-        private val activity: Activity
+        private val context: Context
     ) : Consumer<OEMWindowLayoutInfo> {
         private val multicastConsumerLock = ReentrantLock()
         @GuardedBy("lock")
@@ -127,7 +166,7 @@
 
         override fun accept(value: OEMWindowLayoutInfo) {
             multicastConsumerLock.withLock {
-                lastKnownValue = translate(activity, value)
+                lastKnownValue = translate(context, value)
                 registeredListeners.forEach { consumer -> consumer.accept(lastKnownValue) }
             }
         }
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 2ba93ca..ed9f74d 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
@@ -19,6 +19,9 @@
 import androidx.window.extensions.layout.FoldingFeature as OEMFoldingFeature
 import androidx.window.extensions.layout.WindowLayoutInfo as OEMWindowLayoutInfo
 import android.app.Activity
+import android.content.Context
+import android.os.Build
+import androidx.annotation.UiContext
 import androidx.window.core.Bounds
 import androidx.window.layout.FoldingFeature
 import androidx.window.layout.FoldingFeature.State.Companion.FLAT
@@ -27,11 +30,15 @@
 import androidx.window.layout.HardwareFoldingFeature.Type.Companion.FOLD
 import androidx.window.layout.HardwareFoldingFeature.Type.Companion.HINGE
 import androidx.window.layout.WindowLayoutInfo
+import androidx.window.layout.WindowMetrics
 import androidx.window.layout.WindowMetricsCalculatorCompat.computeCurrentWindowMetrics
 
 internal object ExtensionsWindowLayoutInfoAdapter {
 
-    internal fun translate(activity: Activity, oemFeature: OEMFoldingFeature): FoldingFeature? {
+    internal fun translate(
+        windowMetrics: WindowMetrics,
+        oemFeature: OEMFoldingFeature,
+    ): FoldingFeature? {
         val type = when (oemFeature.type) {
             OEMFoldingFeature.TYPE_FOLD -> FOLD
             OEMFoldingFeature.TYPE_HINGE -> HINGE
@@ -43,17 +50,36 @@
             else -> return null
         }
         val bounds = Bounds(oemFeature.bounds)
-        return if (validBounds(activity, bounds)) {
+        return if (validBounds(windowMetrics, bounds)) {
             HardwareFoldingFeature(Bounds(oemFeature.bounds), type, state)
         } else {
             null
         }
     }
 
-    internal fun translate(activity: Activity, info: OEMWindowLayoutInfo): WindowLayoutInfo {
+    internal fun translate(
+        @UiContext context: Context,
+        info: OEMWindowLayoutInfo,
+    ): WindowLayoutInfo {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            translate(computeCurrentWindowMetrics(context), info)
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (context is Activity)) {
+            translate(computeCurrentWindowMetrics(context), info)
+        } else {
+            throw UnsupportedOperationException(
+                "Display Features are only supported after Q. Display features for non-Activity " +
+                    "contexts are not expected to be reported on devices running Q."
+            )
+        }
+    }
+
+    internal fun translate(
+        windowMetrics: WindowMetrics,
+        info: OEMWindowLayoutInfo
+    ): WindowLayoutInfo {
         val features = info.displayFeatures.mapNotNull { feature ->
             when (feature) {
-                is OEMFoldingFeature -> translate(activity, feature)
+                is OEMFoldingFeature -> translate(windowMetrics, feature)
                 else -> null
             }
         }
@@ -61,19 +87,17 @@
     }
 
     /**
-     * Validate the bounds for a [FoldingFeature] within a given [Activity]. Check the following
-     * <ul>
-     *     <li>Bounds are not 0</li>
-     *     <li>Bounds are either full width or full height</li>
-     *     <li>Bounds do not take up the entire window</li>
-     * </ul>
-     *
-     * @param activity housing the [FoldingFeature].
+     * Checks the bounds for a [FoldingFeature] within a given [WindowMetrics]. Validates the
+     * following:
+     *  - [Bounds] are not `0`
+     *  - [Bounds] are either full width or full height
+     *  - [Bounds] do not take up the entire [windowMetrics]
+     * @param windowMetrics Extracted from a [UiContext] housing the [FoldingFeature].
      * @param bounds the bounds of a [FoldingFeature]
-     * @return true if the bounds are valid for the [Activity], false otherwise.
+     * @return true if the bounds are valid for the [WindowMetrics], false otherwise.
      */
-    private fun validBounds(activity: Activity, bounds: Bounds): Boolean {
-        val windowBounds = computeCurrentWindowMetrics(activity).bounds
+    private fun validBounds(windowMetrics: WindowMetrics, bounds: Bounds): Boolean {
+        val windowBounds = windowMetrics.bounds
         if (bounds.isZero) {
             return false
         }
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 fa9f015..79fd244 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
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.util.Log
 import androidx.annotation.GuardedBy
+import androidx.annotation.UiContext
 import androidx.annotation.VisibleForTesting
 import androidx.core.util.Consumer
 import androidx.window.core.Version
@@ -54,37 +55,43 @@
     }
 
     override fun registerLayoutChangeCallback(
-        activity: Activity,
+        @UiContext context: Context,
         executor: Executor,
         callback: Consumer<WindowLayoutInfo>
     ) {
-        globalLock.withLock {
-            val windowExtension = windowExtension
-            if (windowExtension == null) {
-                if (DEBUG) {
-                    Log.v(TAG, "Extension not loaded, skipping callback registration.")
+        val activity = context as? Activity
+        activity?.let {
+            globalLock.withLock {
+                val windowExtension = windowExtension
+                if (windowExtension == null) {
+                    if (DEBUG) {
+                        Log.v(TAG, "Extension not loaded, skipping callback registration.")
+                    }
+                    callback.accept(WindowLayoutInfo(emptyList()))
+                    return
                 }
-                callback.accept(WindowLayoutInfo(emptyList()))
-                return
-            }
 
-            // Check if the activity was already registered, in case we need to report tracking of
-            // a new activity to the extension.
-            val isActivityRegistered = isActivityRegistered(activity)
-            val callbackWrapper = WindowLayoutChangeCallbackWrapper(activity, executor, callback)
-            windowLayoutChangeCallbacks.add(callbackWrapper)
-            if (!isActivityRegistered) {
-                windowExtension.onWindowLayoutChangeListenerAdded(activity)
-            } else {
-                // Latest info for the previously registered callback for activity
-                // and send it to the new activity
-                val lastInfo = windowLayoutChangeCallbacks.firstOrNull {
-                    activity == it.activity
-                }?.lastInfo
-                if (lastInfo != null) {
-                    callbackWrapper.accept(lastInfo)
+                // Check if the activity was already registered, in case we need to report tracking
+                // of a new activity to the extension.
+                val isActivityRegistered = isActivityRegistered(activity)
+                val callbackWrapper =
+                    WindowLayoutChangeCallbackWrapper(activity, executor, callback)
+                windowLayoutChangeCallbacks.add(callbackWrapper)
+                if (!isActivityRegistered) {
+                    windowExtension.onWindowLayoutChangeListenerAdded(activity)
+                } else {
+                    // Latest info for the previously registered callback for activity
+                    // and send it to the new activity
+                    val lastInfo = windowLayoutChangeCallbacks.firstOrNull {
+                        activity == it.activity
+                    }?.lastInfo
+                    if (lastInfo != null) {
+                        callbackWrapper.accept(lastInfo)
+                    }
                 }
             }
+        } ?: run {
+            callback.accept(WindowLayoutInfo(emptyList()))
         }
     }
 
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoItem.kt b/window/window/src/main/java/androidx/window/layout/util/ActivityCompatHelperApi24.kt
similarity index 68%
copy from window/window-samples/src/main/java/androidx/window/sample/demos/DemoItem.kt
copy to window/window/src/main/java/androidx/window/layout/util/ActivityCompatHelperApi24.kt
index fb08562..a4117f0 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/DemoItem.kt
+++ b/window/window/src/main/java/androidx/window/layout/util/ActivityCompatHelperApi24.kt
@@ -13,8 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.window.sample.demos
+
+package androidx.window.layout.util
 
 import android.app.Activity
+import android.os.Build
+import androidx.annotation.RequiresApi
 
-class DemoItem(val description: String, val buttonTitle: String, val clazz: Class<out Activity>)
\ No newline at end of file
+@RequiresApi(Build.VERSION_CODES.N)
+internal object ActivityCompatHelperApi24 {
+    fun isInMultiWindowMode(activity: Activity): Boolean {
+        return activity.isInMultiWindowMode
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/layout/util/ActivityCompatHelper.kt b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
similarity index 61%
rename from window/window/src/main/java/androidx/window/layout/util/ActivityCompatHelper.kt
rename to window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
index aafd1f9..eaafa91 100644
--- a/window/window/src/main/java/androidx/window/layout/util/ActivityCompatHelper.kt
+++ b/window/window/src/main/java/androidx/window/layout/util/ContextCompatHelper.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -16,28 +16,26 @@
 
 package androidx.window.layout.util
 
-import android.app.Activity
+import android.content.Context
 import android.graphics.Rect
 import android.os.Build
+import android.view.WindowManager
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
+import androidx.annotation.UiContext
 import androidx.core.view.WindowInsetsCompat
 
-@RequiresApi(Build.VERSION_CODES.N)
-internal object ActivityCompatHelperApi24 {
-    fun isInMultiWindowMode(activity: Activity): Boolean {
-        return activity.isInMultiWindowMode
-    }
-}
-
 @RequiresApi(Build.VERSION_CODES.R)
-internal object ActivityCompatHelperApi30 {
-    fun currentWindowBounds(activity: Activity): Rect {
-        return activity.windowManager.currentWindowMetrics.bounds
+internal object ContextCompatHelper {
+
+    fun currentWindowBounds(@UiContext context: Context): Rect {
+        val wm = context.getSystemService(WindowManager::class.java)
+        return wm.currentWindowMetrics.bounds
     }
 
-    fun maximumWindowBounds(activity: Activity): Rect {
-        return activity.windowManager.maximumWindowMetrics.bounds
+    fun maximumWindowBounds(@UiContext context: Context): Rect {
+        val wm = context.getSystemService(WindowManager::class.java)
+        return wm.maximumWindowMetrics.bounds
     }
 
     /**
@@ -48,8 +46,9 @@
      * @see androidx.window.layout.WindowMetrics.getWindowInsets
      */
     @DoNotInline
-    fun currentWindowInsets(activity: Activity): WindowInsetsCompat {
-        val platformInsets = activity.windowManager.currentWindowMetrics.windowInsets
+    fun currentWindowInsets(@UiContext context: Context): WindowInsetsCompat {
+        val wm = context.getSystemService(WindowManager::class.java)
+        val platformInsets = wm.currentWindowMetrics.windowInsets
         return WindowInsetsCompat.toWindowInsetsCompat(platformInsets)
     }
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/layout/util/ContextUtils.kt b/window/window/src/main/java/androidx/window/layout/util/ContextUtils.kt
new file mode 100644
index 0000000..070bdf4
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/layout/util/ContextUtils.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.window.layout.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.inputmethodservice.InputMethodService
+import androidx.annotation.UiContext
+
+internal object ContextUtils {
+    /**
+     * Given a [UiContext], check if it is a [ContextWrapper]. If so, we need to unwrap it and
+     * return the actual [UiContext] within.
+     */
+    @UiContext
+    internal fun unwrapUiContext(@UiContext context: Context): Context {
+        var iterator = context
+
+        while (iterator is ContextWrapper) {
+            if (iterator is Activity) {
+                // Activities are always ContextWrappers
+                return iterator
+            } else if (iterator is InputMethodService) {
+                // InputMethodService are always ContextWrappers
+                return iterator
+            } else if (iterator.baseContext == null) {
+                return iterator
+            }
+
+            iterator = iterator.baseContext
+        }
+
+        // TODO(b/259148796): This code path is not needed for APIs R and above. However, that is
+        //  not clear and also not enforced anywhere. Once we move to version-based implementations,
+        //  this ambiguity will no longer exist. Again for clarity, on APIs before R, UiContexts are
+        //  Activities or InputMethodServices, so we should never reach this point.
+        throw IllegalArgumentException("Context $context is not a UiContext")
+    }
+}
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index 612d1f2..2d0bb8b 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -14,26 +14,36 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<resources>
-    <!-- Defines what part of the width should be given to the primary activity. Defaults to an
-    equal width split. -->
+<resources xmlns:xs="http://schemas.android.com/apk/res/android">
+    <!-- Defines what activity container should be given to the primary part of the task
+     bounds. Values in range (0.0, 1.0) define the size of the primary container of the split
+     relative to the corresponding task dimension size. 0.0 means the secondary activity container
+     fills the full Task bounds, and occludes the primary activity container, which is also expanded
+     to fill the full Task bounds. 0.5 means the primary and secondary container shares an equal
+     split. Ratio larger than `0.5` means the primary container takes more partition. Otherwise,
+     the secondary container takes more partition. -->
     <attr name="splitRatio" format="float" />
     <!-- The smallest value of width of the parent window when the split should be used. -->
     <attr name="splitMinWidthDp" format="integer" />
+    <!-- The smallest value of height of the parent window when the split should be used. -->
+    <attr name="splitMinHeightDp" format="integer" />
     <!-- The smallest value of the smallest-width (sw) of the parent window in any rotation when
      the split should be used. -->
     <attr name="splitMinSmallestWidthDp" format="integer" />
-    <!-- The layout direction for the split. The value must be one of "ltr", "rtl" or "locale". -->
     <attr name="splitLayoutDirection" format="enum">
-        <!-- It splits the task bounds vertically, and the direction is deduced from the default
-        language script of locale, which can be either "ltr" or "rtl". -->
-        <enum name="locale" value="3" />
-        <!-- It splits the task bounds vertically, and put the primary container on the left
-        portion, and the secondary container on the right portion. -->
-        <enum name="ltr" value="0" />
-        <!-- It splits the task bounds vertically, and put the primary container on the right
-        portion, and the secondary container on the left portion. -->
-        <enum name="rtl" value="1" />
+        <!-- A layout direction that splits the task bounds vertically, and the direction is deduced
+        from the language script of locale. The direction can be either rtl or ltr -->
+        <enum name="locale" value="0" />
+        <!-- The primary container is placed on the left, and the secondary is on the right hand
+        side. -->
+        <enum name="ltr" value="1" />
+        <!-- The primary container is placed on the right, and the secondary is on the left hand
+        side. -->
+        <enum name="rtl" value="2" />
+        <!-- The primary container is placed on top, and the secondary is at the bottom. -->
+        <enum name="topToBottom" value="3" />
+        <!-- The primary container is placed on bottom, and the secondary is at the top. -->
+        <enum name="bottomToTop" value="4" />
     </attr>
     <attr name="finishPrimaryWithSecondary" format="enum">
         <enum name="never" value="0" />
@@ -49,6 +59,15 @@
         <enum name="always" value="1" />
         <enum name="adjacent" value="2" />
     </attr>
+    <!-- An optional but unique string to identify a SplitPairRule, SplitPlaceholderRule or
+    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 right split rule in
+    SplitAttributesCalculator#computeSplitAttributesForState.-->
+    <attr name="tag" format="string" />
+    <!-- Background color of animation if the animation requires a background. Defaults to the
+     theme's windowBackground. -->
+    <attr name="animationBackgroundColor" format="color" />
 
     <!-- Split configuration rules for activity pairs. Must contain at least one SplitPairFilter.
     See androidx.window.embedding.SplitPairRule for complete documentation. -->
@@ -66,8 +85,11 @@
         <attr name="clearTop" format="boolean" />
         <attr name="splitRatio"/>
         <attr name="splitMinWidthDp"/>
+        <attr name="splitMinHeightDp"/>
         <attr name="splitMinSmallestWidthDp"/>
         <attr name="splitLayoutDirection"/>
+        <attr name="tag"/>
+        <attr name="animationBackgroundColor"/>
     </declare-styleable>
 
     <!-- Configuration rules for split placeholders. Must contain at least one ActivityFilter for
@@ -81,11 +103,14 @@
         <attr name="stickyPlaceholder" format="boolean" />
         <!-- When all activities are finished in the secondary container, the activity in the
          primary container that created the split should also be finished. Defaults to "always". -->
-        <attr name="finishPrimaryWithPlaceholder" />
+        <attr name="finishPrimaryWithPlaceholder"/>
         <attr name="splitRatio"/>
         <attr name="splitMinWidthDp"/>
+        <attr name="splitMinHeightDp"/>
         <attr name="splitMinSmallestWidthDp"/>
         <attr name="splitLayoutDirection"/>
+        <attr name="tag"/>
+        <attr name="animationBackgroundColor"/>
     </declare-styleable>
 
     <!-- Filter used to find if a pair of activities should be put in a split. -->
@@ -107,6 +132,7 @@
     <declare-styleable name="ActivityRule">
         <!-- Whether the activity should always be expanded on launch. -->
         <attr name="alwaysExpand" format="boolean" />
+        <attr name="tag"/>
     </declare-styleable>
 
     <!-- Filter for ActivityRule. -->
diff --git a/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt b/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt
new file mode 100644
index 0000000..04d3702
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/area/WindowAreaStatusUnitTest.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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.window.area
+
+import androidx.window.core.ExperimentalWindowApi
+import androidx.window.extensions.area.WindowAreaComponent
+import org.junit.Test
+
+/**
+ * Unit tests for [WindowAreaStatus] that run on the JVM.
+ */
+@OptIn(ExperimentalWindowApi::class)
+class WindowAreaStatusUnitTest {
+
+    @Test
+    fun testWindowAreaStatusTranslateValueAvailable() {
+        val expected = WindowAreaStatus.AVAILABLE
+        val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_AVAILABLE)
+        assert(expected == translateValue)
+    }
+
+    @Test
+    fun testWindowAreaStatusTranslateValueUnavailable() {
+        val expected = WindowAreaStatus.UNAVAILABLE
+        val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_UNAVAILABLE)
+        assert(expected == translateValue)
+    }
+
+    @Test
+    fun testWindowAreaStatusTranslateValueUnsupported() {
+        val expected = WindowAreaStatus.UNSUPPORTED
+        val translateValue = WindowAreaStatus.translate(WindowAreaComponent.STATUS_UNSUPPORTED)
+        assert(expected == translateValue)
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt b/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt
index 57709d2..cc18d1e 100644
--- a/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt
+++ b/window/window/src/test/java/androidx/window/core/ConsumerAdapterTest.kt
@@ -18,6 +18,8 @@
 
 import android.annotation.SuppressLint
 import android.app.Activity
+import android.content.Context
+import android.os.Build
 import org.mockito.kotlin.mock
 import java.util.function.Consumer
 import org.junit.Assert.assertEquals
@@ -38,7 +40,12 @@
         }
 
         @Suppress("UNUSED_PARAMETER")
-        fun addConsumer(a: Activity, c: Consumer<String>) {
+        fun addConsumer(context: Context, c: Consumer<String>) {
+            consumers.add(c)
+        }
+
+        @Suppress("UNUSED_PARAMETER")
+        fun addConsumer(activity: Activity, c: Consumer<String>) {
             consumers.add(c)
         }
 
@@ -85,6 +92,29 @@
     }
 
     @Test
+    fun testSubscribeByReflectionForContext() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            // java.util.function.Consumer#accept is supported after N.
+            return
+        }
+        val values = mutableListOf<String>()
+        val context = mock<Context>()
+        adapter.createSubscription(
+            listenerInterface,
+            String::class,
+            "addConsumer",
+            "removeConsumer",
+            context
+        ) { s: String ->
+            values.add(s)
+        }
+
+        assertEquals(1, listenerInterface.consumers.size)
+        listenerInterface.consumers.first().accept(value)
+        assertEquals(listOf(value), values)
+    }
+
+    @Test
     fun testDisposeSubscribe() {
         val values = mutableListOf<String>()
         val subscription = adapter.createSubscription(
@@ -102,6 +132,24 @@
     }
 
     @Test
+    fun testDisposeSubscribeForContext() {
+        val values = mutableListOf<String>()
+        val context = mock<Context>()
+        val subscription = adapter.createSubscription(
+            listenerInterface,
+            String::class,
+            "addConsumer",
+            "removeConsumer",
+            context
+        ) { s: String ->
+            values.add(s)
+        }
+        subscription.dispose()
+
+        assertTrue(listenerInterface.consumers.isEmpty())
+    }
+
+    @Test
     fun testToStringAdd() {
         val values = mutableListOf<String>()
         val consumer: (String) -> Unit = { s: String -> values.add(s) }
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
new file mode 100644
index 0000000..96571cc
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/SplitAttributesTest.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.window.embedding
+
+import android.graphics.Color
+import androidx.window.core.WindowStrictModeException
+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
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.RIGHT_TO_LEFT
+import androidx.window.embedding.SplitAttributes.LayoutDirection.Companion.TOP_TO_BOTTOM
+import androidx.window.embedding.SplitAttributes.SplitType
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+/** Test class to verify [SplitAttributes] */
+class SplitAttributesTest {
+    @Test
+    fun testSplitAttributesEquals() {
+        val attrs1 = SplitAttributes.Builder()
+            .setSplitType(SplitType.splitEqually())
+            .setLayoutDirection(LOCALE)
+            .setAnimationBackgroundColor(0)
+            .build()
+        val attrs2 = SplitAttributes.Builder()
+            .setSplitType(SplitType.splitByHinge())
+            .setLayoutDirection(LOCALE)
+            .setAnimationBackgroundColor(0)
+            .build()
+        val attrs3 = SplitAttributes.Builder()
+            .setSplitType(SplitType.splitByHinge())
+            .setLayoutDirection(TOP_TO_BOTTOM)
+            .setAnimationBackgroundColor(0)
+            .build()
+        val attrs4 = SplitAttributes.Builder()
+            .setSplitType(SplitType.splitByHinge())
+            .setLayoutDirection(TOP_TO_BOTTOM)
+            .setAnimationBackgroundColor(Color.GREEN)
+            .build()
+        val attrs5 = SplitAttributes.Builder()
+            .setSplitType(SplitType.splitByHinge())
+            .setLayoutDirection(TOP_TO_BOTTOM)
+            .setAnimationBackgroundColor(Color.GREEN)
+            .build()
+
+        assertNotEquals(attrs1, attrs2)
+        assertNotEquals(attrs1.hashCode(), attrs2.hashCode())
+
+        assertNotEquals(attrs2, attrs3)
+        assertNotEquals(attrs2.hashCode(), attrs3.hashCode())
+
+        assertNotEquals(attrs3, attrs1)
+        assertNotEquals(attrs3.hashCode(), attrs1.hashCode())
+
+        assertNotEquals(attrs3, attrs4)
+        assertNotEquals(attrs3.hashCode(), attrs4.hashCode())
+
+        assertEquals(attrs4, attrs5)
+        assertEquals(attrs4.hashCode(), attrs5.hashCode())
+    }
+
+    @Test
+    fun testTypesEquals() {
+        val splitTypes = arrayOf(
+            SplitType.splitEqually(),
+            SplitType.expandContainers(),
+            SplitType.splitByHinge(),
+            SplitType.splitByHinge(SplitType.expandContainers())
+        )
+
+        for ((i, type1) in splitTypes.withIndex()) {
+            if (type1 is SplitType.RatioSplitType) {
+                assertEquals(
+                    "Two SplitTypes must regarded as equal if their ratios are the same.",
+                    type1, SplitType.ratio(type1.value)
+                )
+                assertEquals(type1.hashCode(), SplitType.ratio(type1.value).hashCode())
+            }
+            for ((j, type2) in splitTypes.withIndex()) {
+                if (i == j) {
+                    assertEquals(type1, type2)
+                    assertEquals(type1.hashCode(), type2.hashCode())
+                } else {
+                    assertNotEquals(type1, type2)
+                    assertNotEquals(type1.hashCode(), type2.hashCode())
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testSplitRatioRatio() {
+        assertThrows(WindowStrictModeException::class.java) {
+            SplitType.ratio(-0.01f)
+        }
+        assertThrows(WindowStrictModeException::class.java) {
+            SplitType.ratio(0.0f)
+        }
+        SplitType.ratio(0.001f)
+        SplitType.ratio(0.5f)
+        SplitType.ratio(0.999f)
+        assertThrows(WindowStrictModeException::class.java) {
+            SplitType.ratio(1.0f)
+        }
+        assertThrows(WindowStrictModeException::class.java) {
+            SplitType.ratio(1.1f)
+        }
+    }
+
+    @Test
+    fun testLayoutDirectionEquals() {
+        val layoutDirectionList = arrayOf(
+            LOCALE,
+            LEFT_TO_RIGHT,
+            RIGHT_TO_LEFT,
+            TOP_TO_BOTTOM,
+            BOTTOM_TO_TOP,
+        )
+
+        for ((i, layoutDirection1) in layoutDirectionList.withIndex()) {
+            for ((j, layoutDirection2) in layoutDirectionList.withIndex()) {
+                if (i == j) {
+                    assertEquals(layoutDirection1, layoutDirection2)
+                    assertEquals(layoutDirection1.hashCode(), layoutDirection2.hashCode())
+                } else {
+                    assertNotEquals(layoutDirection1, layoutDirection2)
+                    assertNotEquals(layoutDirection1.hashCode(), layoutDirection2.hashCode())
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
index 8f15b16..06c6bbc 100644
--- a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
@@ -29,7 +29,8 @@
         val activity = mock<Activity>()
         val firstStack = ActivityStack(listOf(activity))
         val secondStack = ActivityStack(emptyList())
-        val info = SplitInfo(firstStack, secondStack, 0.5f)
+        val attributes = SplitAttributes()
+        val info = SplitInfo(firstStack, secondStack, attributes)
 
         assertTrue(info.contains(activity))
     }
@@ -39,7 +40,8 @@
         val activity = mock<Activity>()
         val firstStack = ActivityStack(emptyList())
         val secondStack = ActivityStack(listOf(activity))
-        val info = SplitInfo(firstStack, secondStack, 0.5f)
+        val attributes = SplitAttributes()
+        val info = SplitInfo(firstStack, secondStack, attributes)
 
         assertTrue(info.contains(activity))
     }
@@ -49,8 +51,9 @@
         val activity = mock<Activity>()
         val firstStack = ActivityStack(emptyList())
         val secondStack = ActivityStack(listOf(activity))
-        val firstInfo = SplitInfo(firstStack, secondStack, 0.5f)
-        val secondInfo = SplitInfo(firstStack, secondStack, 0.5f)
+        val attributes = SplitAttributes()
+        val firstInfo = SplitInfo(firstStack, secondStack, attributes)
+        val secondInfo = SplitInfo(firstStack, secondStack, attributes)
 
         assertEquals(firstInfo, secondInfo)
         assertEquals(firstInfo.hashCode(), secondInfo.hashCode())
diff --git a/window/window/src/test/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendUnitTest.kt b/window/window/src/test/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendUnitTest.kt
index a5d698f..f286aa1 100644
--- a/window/window/src/test/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendUnitTest.kt
+++ b/window/window/src/test/java/androidx/window/layout/adapter/sidecar/SidecarWindowBackendUnitTest.kt
@@ -73,6 +73,18 @@
     }
 
     @Test
+    public fun testRegisterLayoutChangeCallback_withContext() {
+        val backend = SidecarWindowBackend.getInstance(context)
+        backend.windowExtension = mock()
+
+        // Check registering the layout change callback
+        val consumer = mock<Consumer<WindowLayoutInfo>>()
+        val context = mock<Context>()
+        backend.registerLayoutChangeCallback(context, { obj: Runnable -> obj.run() }, consumer)
+        verify(consumer).accept(any())
+    }
+
+    @Test
     public fun testRegisterLayoutChangeCallback_noExtension() {
         val backend = SidecarWindowBackend.getInstance(context)
         backend.windowExtension = null
diff --git a/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt b/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
index 9ec07b1..5206e1e 100644
--- a/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
+++ b/window/window/src/testUtil/java/androidx/window/layout/TestWindowMetricsCalculator.kt
@@ -16,7 +16,9 @@
 package androidx.window.layout
 
 import android.app.Activity
+import android.content.Context
 import android.graphics.Rect
+import androidx.annotation.UiContext
 
 /**
  * Implementation of [WindowMetricsCalculator] for testing.
@@ -24,44 +26,62 @@
  * @see WindowMetricsCalculator
  */
 internal class TestWindowMetricsCalculator : WindowMetricsCalculator {
-    private var globalOverriddenBounds: Rect? = null
-    private val overriddenBounds = mutableMapOf<Activity, Rect?>()
-    private val overriddenMaximumBounds = mutableMapOf<Activity, Rect?>()
+    private var overrideBounds: Rect? = null
+    private var overrideMaxBounds: Rect? = null
+    private val currentBounds = mutableMapOf<Context, Rect>()
+    private val maxBounds = mutableMapOf<Context, Rect>()
 
     /**
-     * Overrides the bounds returned from this helper for the given context. Passing `null` [bounds]
-     * has the effect of clearing the bounds override.
+     * Sets the bounds returned from this helper for the given context.
      *
-     * Note: A global override set as a result of [.setCurrentBounds] takes precedence
-     * over the value set with this method.
+     * Note: An override set via [setOverrideBounds] takes precedence over the values set with
+     * this method.
      */
-    fun setCurrentBoundsForActivity(activity: Activity, bounds: Rect?) {
-        overriddenBounds[activity] = bounds
+    fun setBounds(@UiContext context: Context, currentBounds: Rect, maxBounds: Rect) {
+        this.currentBounds[context] = currentBounds
+        this.maxBounds[context] = maxBounds
     }
 
     /**
-     * Overrides the max bounds returned from this helper for the given context. Passing `null`
-     * [bounds] has the effect of clearing the bounds override.
+     * Clears the bounds that were set via [setBounds] for the given context.
      */
-    fun setMaximumBoundsForActivity(activity: Activity, bounds: Rect?) {
-        overriddenMaximumBounds[activity] = bounds
+    fun clearBounds(@UiContext context: Context) {
+        currentBounds.remove(context)
+        maxBounds.remove(context)
     }
 
     /**
-     * Overrides the bounds returned from this helper for all supplied contexts. Passing null
-     * [bounds] has the effect of clearing the global override.
+     * Overrides the bounds returned from this helper for all supplied contexts.
      */
-    fun setCurrentBounds(bounds: Rect?) {
-        globalOverriddenBounds = bounds
+    fun setOverrideBounds(currentBounds: Rect, maxBounds: Rect) {
+        overrideBounds = currentBounds
+        overrideMaxBounds = maxBounds
+    }
+
+    /**
+     * Clears the overrides that were set in [setOverrideBounds].
+     */
+    fun clearOverrideBounds() {
+        overrideBounds = null
+        overrideMaxBounds = null
     }
 
     override fun computeCurrentWindowMetrics(activity: Activity): WindowMetrics {
-        val bounds = globalOverriddenBounds ?: overriddenBounds[activity] ?: Rect()
+        return computeCurrentWindowMetrics(activity as Context)
+    }
+
+    override fun computeCurrentWindowMetrics(@UiContext context: Context): WindowMetrics {
+        val bounds = overrideBounds ?: currentBounds[context] ?: Rect()
         return WindowMetrics(bounds)
     }
 
     override fun computeMaximumWindowMetrics(activity: Activity): WindowMetrics {
-        return WindowMetrics(overriddenMaximumBounds[activity] ?: Rect())
+        return computeMaximumWindowMetrics(activity as Context)
+    }
+
+    override fun computeMaximumWindowMetrics(@UiContext context: Context): WindowMetrics {
+        val bounds = overrideMaxBounds ?: maxBounds[context] ?: Rect()
+        return WindowMetrics(bounds)
     }
 
     /**
@@ -69,8 +89,9 @@
      * [.setCurrentBoundsForActivity].
      */
     fun reset() {
-        globalOverriddenBounds = null
-        overriddenBounds.clear()
-        overriddenMaximumBounds.clear()
+        overrideBounds = null
+        overrideMaxBounds = null
+        currentBounds.clear()
+        maxBounds.clear()
     }
 }