Merge "Fix Rotary Encoder Stretch" into androidx-main
diff --git a/activity/activity/lint-baseline.xml b/activity/activity/lint-baseline.xml
deleted file mode 100644
index 7ac7250c..0000000
--- a/activity/activity/lint-baseline.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?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">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 33 (current min is 19): `android.provider.MediaStore#getPickImagesMaxLimit`"
-        errorLine1="                    require(maxItems &lt;= MediaStore.getPickImagesMaxLimit()) {"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 33 (current min is 19): `android.provider.MediaStore#getPickImagesMaxLimit`"
-        errorLine1="                MediaStore.getPickImagesMaxLimit()"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 33; however, the containing class androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                    require(maxItems &lt;= MediaStore.getPickImagesMaxLimit()) {"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
-    </issue>
-
-    <issue
-        id="ClassVerificationFailure"
-        message="This call references a method added in API level 33; however, the containing class androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia.Companion is reachable from earlier API levels and will fail run-time class verification."
-        errorLine1="                MediaStore.getPickImagesMaxLimit()"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
-    </issue>
-
-</issues>
diff --git a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
index 47ef4d1..fe92989 100644
--- a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
@@ -734,6 +734,7 @@
         }
 
         @CallSuper
+        @SuppressLint("NewApi", "ClassVerificationFailure")
         override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
             // Check to see if the photo picker is available
             return if (PickVisualMedia.isPhotoPickerAvailable()) {
@@ -783,6 +784,7 @@
              *
              * @see MediaStore.EXTRA_PICK_IMAGES_MAX
              */
+            @SuppressLint("NewApi", "ClassVerificationFailure")
             internal fun getMaxItems() = if (PickVisualMedia.isPhotoPickerAvailable()) {
                 MediaStore.getPickImagesMaxLimit()
             } else {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index b704ffa..9524df3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -347,7 +347,7 @@
                     ":compose:compiler:compiler"
                 )
             } else {
-                project.rootProject.findProject(":compose:compiler:compiler")!!
+                project.rootProject.resolveProject(":compose:compiler:compiler")
             }
         )
         val kotlinPlugin = configuration.incoming.artifactView { view ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index b8e6838..727552a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -160,7 +160,7 @@
                 KotlinClosure1<String, Project>(
                     function = {
                         // this refers to the first parameter of the closure.
-                        project(this)
+                        project.resolveProject(this)
                     }
                 )
             )
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt
new file mode 100644
index 0000000..7858465
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.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.build
+
+import org.gradle.api.Project
+import org.gradle.api.UnknownProjectException
+
+// Resolves the given project, and if it is not found,
+// throws an exception that mentions the active project subset, if any (MAIN, COMPOSE, ...)
+public fun Project.resolveProject(projectSpecification: String): Project {
+    try {
+        return project.project(projectSpecification)
+    } catch (e: UnknownProjectException) {
+        val subset = project.getProjectSubset()
+        val subsetDescription = if (subset == null) {
+            ""
+        } else {
+            " in subset $subset"
+        }
+        throw UnknownProjectException(
+            "Project $projectSpecification not found$subsetDescription",
+            e
+        )
+    }
+}
+
+private fun Project.getProjectSubset(): String? {
+    val prop = project.providers.gradleProperty("androidx.projects")
+    if (prop.isPresent()) {
+        return prop.get().uppercase()
+    }
+
+    val envProp = project.providers.environmentVariable("ANDROIDX_PROJECTS")
+    if (envProp.isPresent()) {
+        return envProp.get().uppercase()
+    }
+    return null
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index b4af3e0..b6d77efc 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -213,7 +213,9 @@
             it?.close()?.let { closingJob ->
                 closingCameraJobs.add(closingJob)
                 closingJob.invokeOnCompletion {
-                    closingCameraJobs.remove(closingJob)
+                    synchronized(lock) {
+                        closingCameraJobs.remove(closingJob)
+                    }
                 }
             }
         }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index cb4f1cd..6d5f28a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -37,6 +37,8 @@
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
 import androidx.camera.camera2.internal.compat.workaround.FlashAvailabilityChecker;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -358,7 +360,8 @@
 
     @Override
     public boolean isZslSupported() {
-        return Build.VERSION.SDK_INT >= 23 && isPrivateReprocessingSupported();
+        return Build.VERSION.SDK_INT >= 23 && isPrivateReprocessingSupported()
+                && (DeviceQuirks.get(ZslDisablerQuirk.class) == null);
     }
 
     @Override
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
index 12ecb98..bcf5423 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
@@ -38,6 +38,8 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
 import androidx.camera.core.ExperimentalGetImage;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.Logger;
@@ -86,6 +88,8 @@
     private boolean mIsZslDisabledByFlashMode = false;
     private boolean mIsPrivateReprocessingSupported = false;
 
+    private boolean mShouldZslDisabledByQuirks = false;
+
     @SuppressWarnings("WeakerAccess")
     SafeCloseImageReaderProxy mReprocessingImageReader;
     private CameraCaptureCallback mMetadataMatchingCaptureCallback;
@@ -102,6 +106,8 @@
 
         mReprocessingInputSizeMap = createReprocessingInputSizeMap(mCameraCharacteristicsCompat);
 
+        mShouldZslDisabledByQuirks = DeviceQuirks.get(ZslDisablerQuirk.class) != null;
+
         mImageRingBuffer = new ZslRingBuffer(
                 RING_BUFFER_CAPACITY,
                 imageProxy -> imageProxy.close());
@@ -139,6 +145,10 @@
             return;
         }
 
+        if (mShouldZslDisabledByQuirks) {
+            return;
+        }
+
         // Due to b/232268355 and feedback from pixel team that private format will have better
         // performance, we will use private only for zsl.
         if (!mIsPrivateReprocessingSupported
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index fe2ab38..706f1a6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -80,6 +80,9 @@
         if (TorchIsClosedAfterImageCapturingQuirk.load()) {
             quirks.add(new TorchIsClosedAfterImageCapturingQuirk());
         }
+        if (ZslDisablerQuirk.load()) {
+            quirks.add(new ZslDisablerQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
new file mode 100644
index 0000000..6a4ec43
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
@@ -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.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Locale;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: 252818931
+ *     Description: On certain devices, the captured image has color issue for reprocessing. We
+ *                  need to disable zero-shutter lag and return false for
+ *                  {@link CameraInfo#isZslSupported()}.
+ *     Device(s): Samsung Fold4
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ZslDisablerQuirk implements Quirk {
+
+    static boolean load() {
+        return isSamsungFold4();
+    }
+
+    private static boolean isSamsungFold4() {
+        return "samsung".equalsIgnoreCase(Build.BRAND)
+                && android.os.Build.MODEL.toUpperCase(Locale.US).startsWith("SM-F936");
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 5cea42a..0062a4b 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -61,6 +61,7 @@
 import org.robolectric.shadow.api.Shadow;
 import org.robolectric.shadows.ShadowCameraCharacteristics;
 import org.robolectric.shadows.ShadowCameraManager;
+import org.robolectric.util.ReflectionHelpers;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -512,6 +513,36 @@
         assertThat(cameraInfo.isZslSupported()).isFalse();
     }
 
+    @Config(minSdk = 23)
+    @Test
+    public void isZslSupported_hasZslDisablerQuirk_returnFalse()
+            throws CameraAccessExceptionCompat {
+        ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-F936B");
+
+        init(/* hasReprocessingCapabilities = */ true);
+
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+                CAMERA0_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.isZslSupported()).isFalse();
+    }
+
+    @Config(minSdk = 23)
+    @Test
+    public void isZslSupported_hasNoZslDisablerQuirk_returnTrue()
+            throws CameraAccessExceptionCompat {
+        ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G973");
+
+        init(/* hasReprocessingCapabilities = */ true);
+
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+                CAMERA0_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.isZslSupported()).isTrue();
+    }
+
     private CameraManagerCompat initCameraManagerWithPhysicalIds(
             List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
         FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
index 33ddaf5..5804642 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
@@ -39,6 +39,7 @@
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadow.api.Shadow
 import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.util.ReflectionHelpers
 
 val YUV_REPROCESSING_MAXIMUM_SIZE = Size(4000, 3000)
 val PRIVATE_REPROCESSING_MAXIMUM_SIZE = Size(3000, 2000)
@@ -49,20 +50,20 @@
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.M)
-public class ZslControlImplTest {
+class ZslControlImplTest {
 
     private lateinit var zslControl: ZslControlImpl
     private lateinit var sessionConfigBuilder: SessionConfig.Builder
 
     @Before
-    public fun setUp() {
+    fun setUp() {
         sessionConfigBuilder = SessionConfig.Builder().also { sessionConfigBuilder ->
             sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
         }
     }
 
     @Test
-    public fun isPrivateReprocessingSupported_addZslConfig() {
+    fun isPrivateReprocessingSupported_addZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = false,
@@ -85,7 +86,7 @@
     }
 
     @Test
-    public fun isYuvReprocessingSupported_notAddZslConfig() {
+    fun isYuvReprocessingSupported_notAddZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = true,
@@ -99,7 +100,7 @@
     }
 
     @Test
-    public fun isJpegNotValidOutputFormat_notAddZslConfig() {
+    fun isJpegNotValidOutputFormat_notAddZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = true,
@@ -113,7 +114,7 @@
     }
 
     @Test
-    public fun isReprocessingNotSupported_notAddZslConfig() {
+    fun isReprocessingNotSupported_notAddZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = false,
@@ -127,7 +128,7 @@
     }
 
     @Test
-    public fun isZslDisabledByUserCaseConfig_notAddZslConfig() {
+    fun isZslDisabledByUserCaseConfig_notAddZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = false,
@@ -142,7 +143,7 @@
     }
 
     @Test
-    public fun isZslDisabledByFlashMode_addZslConfig() {
+    fun isZslDisabledByFlashMode_addZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = false,
@@ -166,7 +167,7 @@
     }
 
     @Test
-    public fun isZslDisabled_clearZslConfig() {
+    fun isZslDisabled_clearZslConfig() {
         zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
             hasCapabilities = true,
             isYuvReprocessingSupported = false,
@@ -182,6 +183,49 @@
         assertThat(zslControl.mReprocessingImageReader).isNull()
     }
 
+    @Test
+    fun hasZslDisablerQuirk_notAddZslConfig() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-F936B")
+
+        zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
+            hasCapabilities = true,
+            isYuvReprocessingSupported = false,
+            isPrivateReprocessingSupported = true,
+            isJpegValidOutputFormat = true
+        ))
+
+        zslControl.addZslConfig(sessionConfigBuilder)
+
+        assertThat(zslControl.mReprocessingImageReader).isNull()
+    }
+
+    @Test
+    fun hasNoZslDisablerQuirk_addZslConfig() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-G973")
+
+        zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
+            hasCapabilities = true,
+            isYuvReprocessingSupported = false,
+            isPrivateReprocessingSupported = true,
+            isJpegValidOutputFormat = true
+        ))
+
+        zslControl.addZslConfig(sessionConfigBuilder)
+
+        assertThat(zslControl.mReprocessingImageReader).isNotNull()
+        assertThat(zslControl.mReprocessingImageReader.imageFormat).isEqualTo(PRIVATE)
+        assertThat(zslControl.mReprocessingImageReader.maxImages).isEqualTo(
+            MAX_IMAGES)
+        assertThat(zslControl.mReprocessingImageReader.width).isEqualTo(
+            PRIVATE_REPROCESSING_MAXIMUM_SIZE.width)
+        assertThat(zslControl.mReprocessingImageReader.height).isEqualTo(
+            PRIVATE_REPROCESSING_MAXIMUM_SIZE.height)
+        assertThat(zslControl.mImageRingBuffer.maxCapacity).isEqualTo(
+            RING_BUFFER_CAPACITY)
+    }
+
     private fun createCameraCharacteristicsCompat(
         hasCapabilities: Boolean,
         isYuvReprocessingSupported: Boolean,
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
index 455c714..9063d8d 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
@@ -13,23 +13,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.camera.core.imagecapture
 
+import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.BitmapFactory.decodeByteArray
 import android.graphics.ImageFormat
+import android.graphics.Rect
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageProxy
 import androidx.camera.core.imagecapture.Utils.CAMERA_CAPTURE_RESULT
 import androidx.camera.core.imagecapture.Utils.CROP_RECT
 import androidx.camera.core.imagecapture.Utils.HEIGHT
 import androidx.camera.core.imagecapture.Utils.OUTPUT_FILE_OPTIONS
-import androidx.camera.core.imagecapture.Utils.ROTATION_DEGREES
 import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
 import androidx.camera.core.imagecapture.Utils.WIDTH
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.CameraCaptureResultImageInfo
-import androidx.camera.core.internal.utils.ImageUtil
+import androidx.camera.core.internal.utils.ImageUtil.jpegImageToJpegByteArray
+import androidx.camera.core.processing.InternalImageProcessor
+import androidx.camera.testing.TestImageUtil.createJpegBytes
+import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
 import androidx.camera.testing.TestImageUtil.createYuvFakeImageProxy
+import androidx.camera.testing.TestImageUtil.getAverageDiff
+import androidx.camera.testing.fakes.GrayscaleImageEffect
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -39,77 +46,95 @@
 import org.junit.runner.RunWith
 
 /**
- * Instrumented tests for [JpegBytes2Image].
+ * Instrumented tests for [ProcessingNode].
  */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 21)
 class ProcessingNodeDeviceTest {
+    @Test
+    fun applyBitmapEffectInMemory_effectApplied() = runBlocking {
+        processJpegAndVerifyEffectApplied(null)
+    }
+
+    @Test
+    fun applyBitmapEffectOnDisk_effectApplied() = runBlocking {
+        processJpegAndVerifyEffectApplied(OUTPUT_FILE_OPTIONS)
+    }
 
     @Test
     fun processYuvInputInMemory_getsJpegOutput() = runBlocking {
-        // Arrange: create a YUV input.
-        val yuvIn = ProcessingNode.In.of(ImageFormat.YUV_420_888)
-        ProcessingNode(mainThreadExecutor()).also { it.transform(yuvIn) }
-        val takePictureCallback = FakeTakePictureCallback()
-        val imageIn = createYuvFakeImageProxy(
-            CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
-            WIDTH,
-            HEIGHT
-        )
-        val processingRequest = ProcessingRequest(
-            { listOf() },
-            /*outputFileOptions=*/ null,
-            CROP_RECT,
-            ROTATION_DEGREES,
-            /*jpegQuality=*/100,
-            SENSOR_TO_BUFFER,
-            takePictureCallback
-        )
-        val input = ProcessingNode.InputPacket.of(processingRequest, imageIn)
-
-        // Act.
-        yuvIn.edge.accept(input)
-        val imageOut = takePictureCallback.getInMemoryResult()
-
-        // Assert: image content is cropped correctly
-        // TODO(b/245940015): verify the content of the restored image.
-        val jpegOut = ImageUtil.jpegImageToJpegByteArray(imageOut)
-        val bitmapOut = decodeByteArray(jpegOut, 0, jpegOut.size)
-        assertThat(bitmapOut.width).isEqualTo(WIDTH)
-        assertThat(bitmapOut.height).isEqualTo(HEIGHT / 2)
+        processYuvAndVerifyOutputSize(null)
     }
 
     @Test
     fun processYuvInputOnDisk_getsJpegOutput() = runBlocking {
-        // Arrange: create a YUV input.
-        val yuvIn = ProcessingNode.In.of(ImageFormat.YUV_420_888)
-        ProcessingNode(mainThreadExecutor()).also { it.transform(yuvIn) }
-        val takePictureCallback = FakeTakePictureCallback()
+        processYuvAndVerifyOutputSize(OUTPUT_FILE_OPTIONS)
+    }
+
+    private suspend fun processYuvAndVerifyOutputSize(outputFileOptions: OutputFileOptions?) {
+        // Arrange: create node with JPEG input and grayscale effect.
+        val node = ProcessingNode(mainThreadExecutor())
+        val nodeIn = ProcessingNode.In.of(ImageFormat.YUV_420_888)
         val imageIn = createYuvFakeImageProxy(
             CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
             WIDTH,
             HEIGHT
         )
+        // Act.
+        val bitmap = processAndGetBitmap(node, nodeIn, imageIn, outputFileOptions)
+        // Assert: image content is cropped correctly
+        // TODO(b/245940015): verify the content of the restored image.
+        assertThat(bitmap.width).isEqualTo(WIDTH)
+        assertThat(bitmap.height).isEqualTo(HEIGHT / 2)
+    }
+
+    private suspend fun processJpegAndVerifyEffectApplied(outputFileOptions: OutputFileOptions?) {
+        // Arrange: create node with JPEG input and grayscale effect.
+        val node = ProcessingNode(
+            mainThreadExecutor(),
+            InternalImageProcessor(GrayscaleImageEffect())
+        )
+        val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG)
+        val imageIn = createJpegFakeImageProxy(
+            CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
+            createJpegBytes(WIDTH, HEIGHT)
+        )
+        // Act.
+        val bitmap = processAndGetBitmap(node, nodeIn, imageIn, outputFileOptions)
+        // Assert: the output is a cropped grayscale image.
+        assertThat(getAverageDiff(bitmap, Rect(0, 0, 320, 240), 0X555555)).isEqualTo(0)
+        assertThat(getAverageDiff(bitmap, Rect(321, 0, WIDTH, 240), 0XAAAAAA)).isEqualTo(0)
+    }
+
+    private suspend fun processAndGetBitmap(
+        node: ProcessingNode,
+        nodeIn: ProcessingNode.In,
+        imageIn: ImageProxy,
+        outputFileOptions: OutputFileOptions?
+    ): Bitmap {
+        // Arrange: create a YUV input.
+        node.transform(nodeIn)
+        val takePictureCallback = FakeTakePictureCallback()
         val processingRequest = ProcessingRequest(
             { listOf() },
-            OUTPUT_FILE_OPTIONS,
+            outputFileOptions,
             CROP_RECT,
-            ROTATION_DEGREES,
+            /*rotationDegrees=*/0, // 0 because exif does not have rotation.
             /*jpegQuality=*/100,
             SENSOR_TO_BUFFER,
             takePictureCallback
         )
         val input = ProcessingNode.InputPacket.of(processingRequest, imageIn)
-
-        // Act.
-        yuvIn.edge.accept(input)
-        val filePath = takePictureCallback.getOnDiskResult().savedUri!!.path!!
-
-        // Assert: image content is cropped correctly
-        // TODO(b/245940015): verify the content of the restored image.
-        val bitmap = BitmapFactory.decodeFile(filePath)
-        assertThat(bitmap.width).isEqualTo(WIDTH)
-        assertThat(bitmap.height).isEqualTo(HEIGHT / 2)
+        // Act and return.
+        nodeIn.edge.accept(input)
+        return if (outputFileOptions == null) {
+            val imageOut = takePictureCallback.getInMemoryResult()
+            val jpegOut = jpegImageToJpegByteArray(imageOut)
+            decodeByteArray(jpegOut, 0, jpegOut.size)
+        } else {
+            val filePath = takePictureCallback.getOnDiskResult().savedUri!!.path!!
+            BitmapFactory.decodeFile(filePath)
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 2457f15..0d4049b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -337,7 +337,7 @@
     /**
      * Whether the software JPEG pipeline will be used.
      */
-    private boolean mUseSoftwareJpeg = false;
+    private boolean mUseSoftwareJpeg = true;
 
     ////////////////////////////////////////////////////////////////////////////////////////////
     // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
@@ -1937,7 +1937,8 @@
         Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
                 cameraId, resolution));
         checkState(mImagePipeline == null);
-        mImagePipeline = new ImagePipeline(config, resolution);
+        // TODO: set CameraEffect
+        mImagePipeline = new ImagePipeline(config, resolution, null);
         checkState(mTakePictureManager == null);
         mTakePictureManager = new TakePictureManager(mImageCaptureControl, mImagePipeline);
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
index af12fa8..8cc2c12 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
@@ -29,8 +29,10 @@
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.CameraEffect;
 import androidx.camera.core.ForwardingImageProxy;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.impl.CaptureBundle;
@@ -40,6 +42,7 @@
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
+import androidx.camera.core.processing.InternalImageProcessor;
 import androidx.core.util.Pair;
 
 import java.util.ArrayList;
@@ -78,9 +81,18 @@
     // ===== public methods =====
 
     @MainThread
+    @VisibleForTesting
     public ImagePipeline(
             @NonNull ImageCaptureConfig useCaseConfig,
             @NonNull Size cameraSurfaceSize) {
+        this(useCaseConfig, cameraSurfaceSize, /*cameraEffect=*/ null);
+    }
+
+    @MainThread
+    public ImagePipeline(
+            @NonNull ImageCaptureConfig useCaseConfig,
+            @NonNull Size cameraSurfaceSize,
+            @Nullable CameraEffect cameraEffect) {
         checkMainThread();
         mUseCaseConfig = useCaseConfig;
         mCaptureConfig = CaptureConfig.Builder.createFrom(useCaseConfig).build();
@@ -89,7 +101,8 @@
         mCaptureNode = new CaptureNode();
         mBundlingNode = new SingleBundlingNode();
         mProcessingNode = new ProcessingNode(
-                requireNonNull(mUseCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor())));
+                requireNonNull(mUseCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor())),
+                cameraEffect != null ? new InternalImageProcessor(cameraEffect) : null);
 
         // Connect nodes
         mPipelineIn = CaptureNode.In.of(cameraSurfaceSize, mUseCaseConfig.getInputFormat());
@@ -133,6 +146,7 @@
 
     /**
      * Sets a listener for close calls on this image.
+     *
      * @param listener to set
      */
     @MainThread
@@ -270,4 +284,9 @@
         return mCaptureNode;
     }
 
+    @NonNull
+    @VisibleForTesting
+    ProcessingNode getProcessingNode() {
+        return mProcessingNode;
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
index 5e12339..fbe5b37 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
@@ -20,13 +20,16 @@
 
 import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
 
 import static java.util.Objects.requireNonNull;
 
 import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
@@ -35,6 +38,7 @@
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.processing.Edge;
+import androidx.camera.core.processing.InternalImageProcessor;
 import androidx.camera.core.processing.Node;
 import androidx.camera.core.processing.Operation;
 import androidx.camera.core.processing.Packet;
@@ -54,6 +58,8 @@
 
     @NonNull
     private final Executor mBlockingExecutor;
+    @Nullable
+    final InternalImageProcessor mImageProcessor;
 
     private Operation<InputPacket, Packet<ImageProxy>> mInput2Packet;
     private Operation<Image2JpegBytes.In, Packet<byte[]>> mImage2JpegBytes;
@@ -62,13 +68,26 @@
     private Operation<Packet<byte[]>, Packet<Bitmap>> mJpegBytes2CroppedBitmap;
     private Operation<Packet<ImageProxy>, ImageProxy> mJpegImage2Result;
     private Operation<Packet<byte[]>, Packet<ImageProxy>> mJpegBytes2Image;
+    private Operation<Packet<Bitmap>, Packet<Bitmap>> mBitmapEffect;
 
     /**
      * @param blockingExecutor a executor that can be blocked by long running tasks. e.g.
      *                         {@link CameraXExecutors#ioExecutor()}
      */
+    @VisibleForTesting
     ProcessingNode(@NonNull Executor blockingExecutor) {
+        this(blockingExecutor, /*imageProcessor=*/null);
+    }
+
+    /**
+     * @param blockingExecutor a executor that can be blocked by long running tasks. e.g.
+     *                         {@link CameraXExecutors#ioExecutor()}
+     * @param imageProcessor   external effect for post-processing.
+     */
+    ProcessingNode(@NonNull Executor blockingExecutor,
+            @Nullable InternalImageProcessor imageProcessor) {
         mBlockingExecutor = blockingExecutor;
+        mImageProcessor = imageProcessor;
     }
 
     @NonNull
@@ -90,9 +109,15 @@
         mBitmap2JpegBytes = new Bitmap2JpegBytes();
         mJpegBytes2Disk = new JpegBytes2Disk();
         mJpegImage2Result = new JpegImage2Result();
-        if (inputEdge.getFormat() == YUV_420_888) {
+        if (inputEdge.getFormat() == YUV_420_888 || mImageProcessor != null) {
+            // Convert JPEG bytes to ImageProxy for:
+            // - YUV input: YUV -> JPEG -> ImageProxy
+            // - Effects: JPEG -> Bitmap -> effect -> Bitmap -> JPEG -> ImageProxy
             mJpegBytes2Image = new JpegBytes2Image();
         }
+        if (mImageProcessor != null) {
+            mBitmapEffect = new BitmapEffect(mImageProcessor);
+        }
         // No output. The request callback will be invoked to deliver the final result.
         return null;
     }
@@ -131,10 +156,8 @@
         Packet<ImageProxy> originalImage = mInput2Packet.apply(inputPacket);
         Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
                 Image2JpegBytes.In.of(originalImage, request.getJpegQuality()));
-        if (jpegBytes.hasCropping()) {
-            Packet<Bitmap> croppedBitmap = mJpegBytes2CroppedBitmap.apply(jpegBytes);
-            jpegBytes = mBitmap2JpegBytes.apply(
-                    Bitmap2JpegBytes.In.of(croppedBitmap, request.getJpegQuality()));
+        if (jpegBytes.hasCropping() || mBitmapEffect != null) {
+            jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
         }
         return mJpegBytes2Disk.apply(
                 JpegBytes2Disk.In.of(jpegBytes, requireNonNull(request.getOutputFileOptions())));
@@ -146,15 +169,33 @@
             throws ImageCaptureException {
         ProcessingRequest request = inputPacket.getProcessingRequest();
         Packet<ImageProxy> image = mInput2Packet.apply(inputPacket);
-        if (image.getFormat() == YUV_420_888) {
-            Packet<byte[]> jpegPacket = mImage2JpegBytes.apply(
+        if (image.getFormat() == YUV_420_888 || mBitmapEffect != null) {
+            Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
                     Image2JpegBytes.In.of(image, request.getJpegQuality()));
-            image = mJpegBytes2Image.apply(jpegPacket);
+            if (mBitmapEffect != null) {
+                jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
+            }
+            image = mJpegBytes2Image.apply(jpegBytes);
         }
         return mJpegImage2Result.apply(image);
     }
 
     /**
+     * Crops JPEG byte array and apply effect if present.
+     */
+    private Packet<byte[]> cropAndMaybeApplyEffect(Packet<byte[]> jpegPacket, int jpegQuality)
+            throws ImageCaptureException {
+        checkState(jpegPacket.getFormat() == ImageFormat.JPEG);
+        Packet<Bitmap> bitmapPacket = mJpegBytes2CroppedBitmap.apply(jpegPacket);
+        if (mBitmapEffect != null) {
+            // Apply effect if present.
+            bitmapPacket = mBitmapEffect.apply(bitmapPacket);
+        }
+        return mBitmap2JpegBytes.apply(
+                Bitmap2JpegBytes.In.of(bitmapPacket, jpegQuality));
+    }
+
+    /**
      * Sends {@link ImageCaptureException} to {@link TakePictureManager}.
      */
     private static void sendError(@NonNull ProcessingRequest request,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index 4976d49..a40665a 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -44,6 +44,7 @@
 import androidx.camera.core.imagecapture.Utils.injectRotationOptionQuirk
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.CaptureConfig.OPTION_ROTATION
+import androidx.camera.core.impl.ImageCaptureConfig
 import androidx.camera.core.impl.ImageInputConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.IoConfig.OPTION_IO_EXECUTOR
@@ -51,6 +52,7 @@
 import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageReaderProxy
+import androidx.camera.testing.fakes.GrayscaleImageEffect
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -78,6 +80,7 @@
     }
 
     private lateinit var imagePipeline: ImagePipeline
+    private lateinit var imageCaptureConfig: ImageCaptureConfig
 
     @Before
     fun setUp() {
@@ -87,7 +90,8 @@
         }
         builder.mutableConfig.insertOption(OPTION_IO_EXECUTOR, mainThreadExecutor())
         builder.mutableConfig.insertOption(ImageInputConfig.OPTION_INPUT_FORMAT, ImageFormat.JPEG)
-        imagePipeline = ImagePipeline(builder.useCaseConfig, SIZE)
+        imageCaptureConfig = builder.useCaseConfig
+        imagePipeline = ImagePipeline(imageCaptureConfig, SIZE)
     }
 
     @After
@@ -96,6 +100,22 @@
     }
 
     @Test
+    fun createPipelineWithoutEffect_processingNodeHasNoEffect() {
+        assertThat(imagePipeline.processingNode.mImageProcessor).isNull()
+    }
+
+    @Test
+    fun createPipelineWithEffect_processingNodeContainsEffect() {
+        assertThat(
+            ImagePipeline(
+                imageCaptureConfig,
+                SIZE,
+                GrayscaleImageEffect()
+            ).processingNode.mImageProcessor
+        ).isNotNull()
+    }
+
+    @Test
     fun createRequests_verifyCameraRequest() {
         // Arrange.
         val captureInput = imagePipeline.captureNode.inputEdge
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/GrantPermissionRuleUtil.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/GrantPermissionRuleUtil.kt
deleted file mode 100644
index 9d92cd4..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/GrantPermissionRuleUtil.kt
+++ /dev/null
@@ -1,36 +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.camera.testing
-
-import android.Manifest
-import android.os.Build
-import androidx.test.rule.GrantPermissionRule
-
-/**
- * Util functions for grant permission
- */
-object GrantPermissionRuleUtil {
-    /**
-     * Returns a GrantPermissionRule after filtering the available permissions according to API
-     * level.
-     */
-    @JvmStatic
-    fun grantWithApiLevelFilter(vararg permissions: String): GrantPermissionRule =
-        GrantPermissionRule.grant(*(permissions.filter {
-            Build.VERSION.SDK_INT <= 32 || it != Manifest.permission.WRITE_EXTERNAL_STORAGE
-        }).toTypedArray())
-}
\ No newline at end of file
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index f8a2ad5..b591b5e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -9,6 +9,7 @@
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+    field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index f8a2ad5..b591b5e 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -9,6 +9,7 @@
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+    field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index f8a2ad5..b591b5e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -9,6 +9,7 @@
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+    field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
index b3d05d8..327396a 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
@@ -76,17 +76,26 @@
     /**
      * The recording is muted because the audio encoder encountered errors.
      *
-     * <p>When the audio encoder encountered errors, the recording will keep being recorded
-     * without audio for the rest of the recording. The audio stats generated after the audio
-     * encoder failed will contain this audio state.
+     * <p>If the audio source encounters errors during recording, audio stats generated after the
+     * error will contain this audio state, and the recording will proceed without audio.
      *
      * <p>Use {@link #getErrorCause()} to get the error cause.
      */
     public static final int AUDIO_STATE_ENCODER_ERROR = 3;
 
+    /**
+     *  The recording is muted because the audio source encountered errors.
+     *
+     * <p>If the audio source encounters errors during recording, audio stats generated after the
+     * error will contain this audio state, and the recording will proceed without audio.
+     *
+     * <p>Use {@link #getErrorCause()} to get the error cause.
+     */
+    public static final int AUDIO_STATE_SOURCE_ERROR = 4;
+
     /** @hide */
     @IntDef({AUDIO_STATE_ACTIVE, AUDIO_STATE_DISABLED, AUDIO_STATE_SOURCE_SILENCED,
-            AUDIO_STATE_ENCODER_ERROR})
+            AUDIO_STATE_ENCODER_ERROR, AUDIO_STATE_SOURCE_ERROR})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     public @interface AudioState {
@@ -99,7 +108,7 @@
      */
     private static final Set<Integer> ERROR_STATES =
             Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AUDIO_STATE_SOURCE_SILENCED,
-                    AUDIO_STATE_ENCODER_ERROR)));
+                    AUDIO_STATE_ENCODER_ERROR, AUDIO_STATE_SOURCE_ERROR)));
 
     /**
      * Indicates whether the recording is being recorded with audio.
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index d85ba2e..1c7eb36 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -73,7 +73,6 @@
 import androidx.camera.video.StreamInfo.StreamState;
 import androidx.camera.video.internal.AudioSource;
 import androidx.camera.video.internal.AudioSourceAccessException;
-import androidx.camera.video.internal.ResourceCreationException;
 import androidx.camera.video.internal.compat.Api26Impl;
 import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
@@ -220,9 +219,13 @@
          */
         ACTIVE,
         /**
-         * The audio source or the audio encoder encountered errors.
+         * The audio encoder encountered errors.
          */
-        ERROR
+        ERROR_ENCODER,
+        /**
+         * The audio source encountered errors.
+         */
+        ERROR_SOURCE,
     }
 
     /**
@@ -1109,13 +1112,13 @@
     /**
      * Setup audio related resources.
      *
-     * @throws ResourceCreationException if the necessary resource for audio to work failed to be
-     * setup.
+     * @throws AudioSourceAccessException if the audio source failed to be setup.
+     * @throws InvalidConfigException if the audio encoder failed to be setup.
      */
     @RequiresPermission(Manifest.permission.RECORD_AUDIO)
     @ExecutedBy("mSequentialExecutor")
     private void setupAudio(@NonNull RecordingRecord recordingToStart)
-            throws ResourceCreationException {
+            throws AudioSourceAccessException, InvalidConfigException {
         MediaSpec mediaSpec = getObservableData(mMediaSpec);
         // Resolve the audio mime info
         MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedCamcorderProfile);
@@ -1124,26 +1127,18 @@
         // Select and create the audio source
         AudioSource.Settings audioSourceSettings =
                 resolveAudioSourceSettings(audioMimeInfo, mediaSpec.getAudioSpec());
-        try {
-            if (mAudioSource != null) {
-                releaseCurrentAudioSource();
-            }
-            // TODO: set audioSourceTimebase to AudioSource. Currently AudioSource hard code
-            //  AudioTimestamp.TIMEBASE_MONOTONIC.
-            mAudioSource = setupAudioSource(recordingToStart, audioSourceSettings);
-            Logger.d(TAG, String.format("Set up new audio source: 0x%x", mAudioSource.hashCode()));
-        } catch (AudioSourceAccessException e) {
-            throw new ResourceCreationException(e);
+        if (mAudioSource != null) {
+            releaseCurrentAudioSource();
         }
+        // TODO: set audioSourceTimebase to AudioSource. Currently AudioSource hard code
+        //  AudioTimestamp.TIMEBASE_MONOTONIC.
+        mAudioSource = setupAudioSource(recordingToStart, audioSourceSettings);
+        Logger.d(TAG, String.format("Set up new audio source: 0x%x", mAudioSource.hashCode()));
 
         // Select and create the audio encoder
         AudioEncoderConfig audioEncoderConfig = resolveAudioEncoderConfig(audioMimeInfo,
                 audioSourceTimebase, audioSourceSettings, mediaSpec.getAudioSpec());
-        try {
-            mAudioEncoder = mAudioEncoderFactory.createEncoder(mExecutor, audioEncoderConfig);
-        } catch (InvalidConfigException e) {
-            throw new ResourceCreationException(e);
-        }
+        mAudioEncoder = mAudioEncoderFactory.createEncoder(mExecutor, audioEncoderConfig);
 
         // Connect the audio source to the audio encoder
         Encoder.EncoderInput bufferProvider = mAudioEncoder.getInput();
@@ -1158,11 +1153,8 @@
     private AudioSource setupAudioSource(@NonNull RecordingRecord recordingToStart,
             @NonNull AudioSource.Settings audioSourceSettings)
             throws AudioSourceAccessException {
-
-        AudioSource audioSource = recordingToStart.performOneTimeAudioSourceCreation(
-                audioSourceSettings, AUDIO_EXECUTOR);
-
-        return audioSource;
+        return recordingToStart.performOneTimeAudioSourceCreation(audioSourceSettings,
+                AUDIO_EXECUTOR);
     }
 
     private void releaseCurrentAudioSource() {
@@ -1207,7 +1199,7 @@
             mVideoEncoder = mVideoEncoderFactory.createEncoder(mExecutor, config);
         } catch (InvalidConfigException e) {
             Logger.e(TAG, "Unable to initialize video encoder.", e);
-            onEncoderSetupError(new ResourceCreationException(e));
+            onEncoderSetupError(e);
             return;
         }
 
@@ -1477,7 +1469,9 @@
 
         // Configure audio based on the current audio state.
         switch (mAudioState) {
-            case ERROR:
+            case ERROR_ENCODER:
+                // Fall-through
+            case ERROR_SOURCE:
                 // Fall-through
             case ACTIVE:
                 // Fall-through
@@ -1497,9 +1491,15 @@
                     try {
                         setupAudio(recordingToStart);
                         setAudioState(AudioState.ACTIVE);
-                    } catch (ResourceCreationException e) {
+                    } catch (AudioSourceAccessException | InvalidConfigException e) {
                         Logger.e(TAG, "Unable to create audio resource with error: ", e);
-                        setAudioState(AudioState.ERROR);
+                        AudioState audioState;
+                        if (e instanceof InvalidConfigException) {
+                            audioState = AudioState.ERROR_ENCODER;
+                        } else {
+                            audioState = AudioState.ERROR_SOURCE;
+                        }
+                        setAudioState(audioState);
                         mAudioErrorCause = e;
                     }
                 }
@@ -1623,7 +1623,11 @@
                                 // If the audio source or encoder encounters error, update the
                                 // status event to notify users. Then continue recording without
                                 // audio data.
-                                setAudioState(AudioState.ERROR);
+                                if (throwable instanceof EncodeException) {
+                                    setAudioState(AudioState.ERROR_ENCODER);
+                                } else {
+                                    setAudioState(AudioState.ERROR_SOURCE);
+                                }
                                 mAudioErrorCause = throwable;
                                 updateInProgressStatusEvent();
                                 completer.set(null);
@@ -1955,8 +1959,10 @@
                 } else {
                     return AudioStats.AUDIO_STATE_ACTIVE;
                 }
-            case ERROR:
+            case ERROR_ENCODER:
                 return AudioStats.AUDIO_STATE_ENCODER_ERROR;
+            case ERROR_SOURCE:
+                return AudioStats.AUDIO_STATE_SOURCE_ERROR;
             case IDLING:
                 // AudioStats should not be produced when audio is in IDLING state.
                 break;
@@ -2051,7 +2057,9 @@
                 setAudioState(AudioState.IDLING);
                 mAudioSource.stop();
                 break;
-            case ERROR:
+            case ERROR_ENCODER:
+                // Fall-through
+            case ERROR_SOURCE:
                 // Reset audio state to INITIALIZING if the audio encoder encountered error, so
                 // that it can be setup again when the next recording with audio enabled is started.
                 setAudioState(AudioState.INITIALIZING);
@@ -2431,7 +2439,7 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mSequentialExecutor")
-    void setAudioState(AudioState audioState) {
+    void setAudioState(@NonNull AudioState audioState) {
         Logger.d(TAG, "Transitioning audio state: " + mAudioState + " --> " + audioState);
         mAudioState = audioState;
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/ResourceCreationException.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/ResourceCreationException.java
deleted file mode 100644
index 5853fa6..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/ResourceCreationException.java
+++ /dev/null
@@ -1,37 +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.camera.video.internal;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-/** An exception thrown to indicate an error has occurred during creating necessary resource. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class ResourceCreationException extends Exception {
-
-    public ResourceCreationException(@Nullable String message) {
-        super(message);
-    }
-
-    public ResourceCreationException(@Nullable String message, @Nullable Throwable cause) {
-        super(message, cause);
-    }
-
-    public ResourceCreationException(@Nullable Throwable cause) {
-        super(cause);
-    }
-}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 41b0c0e..2a9f7f3 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -89,6 +89,9 @@
         if (ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.load()) {
             quirks.add(new ImageCaptureFailedWhenVideoCaptureIsBoundQuirk());
         }
+        if (MediaCodecDoesNotSendEos.load()) {
+            quirks.add(new MediaCodecDoesNotSendEos());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java
index a72f29f..e4b6a17 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java
@@ -26,13 +26,13 @@
  *     Bug Id: b/239369953
  *     Description: When taking image with VideoCapture is bound, the capture result is returned
  *                  but the resulting image can not be obtained.
- *     Device(s): BLU Studio X10, Itel w6004, and Vivo 1805.
+ *     Device(s): BLU Studio X10, Itel w6004, Twist 2 Pro, and Vivo 1805.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ImageCaptureFailedWhenVideoCaptureIsBoundQuirk implements Quirk {
 
     static boolean load() {
-        return isBluStudioX10() || isItelW6004() || isVivo1805();
+        return isBluStudioX10() || isItelW6004() || isVivo1805() || isPositivoTwist2Pro();
     }
 
     public static boolean isBluStudioX10() {
@@ -46,4 +46,9 @@
     public static boolean isVivo1805() {
         return "vivo".equalsIgnoreCase(Build.BRAND) && "vivo 1805".equalsIgnoreCase(Build.MODEL);
     }
+
+    public static boolean isPositivoTwist2Pro() {
+        return "positivo".equalsIgnoreCase(Build.BRAND) && "twist 2 pro".equalsIgnoreCase(
+                Build.MODEL);
+    }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
new file mode 100644
index 0000000..46ccb85
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
@@ -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.camera.video.internal.compat.quirk;
+
+import android.media.MediaCodec;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: b/248189542
+ * Description: When recording video with effect pipeline enabled, calling
+ *              {@link MediaCodec#signalEndOfInputStream()} doesn't trigger an EOS buffer to
+ *              {@link MediaCodec.Callback}.
+ * Device(s): twist 2 pro.
+ */
+@RequiresApi(21)
+public class MediaCodecDoesNotSendEos implements Quirk {
+
+    public static boolean isPositivoTwist2Pro() {
+        return "positivo".equalsIgnoreCase(Build.BRAND) && "twist 2 pro".equalsIgnoreCase(
+                Build.MODEL);
+    }
+
+    static boolean load() {
+        return isPositivoTwist2Pro();
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index bf72036..c5d5bf1 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -26,6 +26,8 @@
 import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.STARTED;
 import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.STOPPING;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.SuppressLint;
 import android.media.MediaCodec;
 import android.media.MediaCodecInfo;
@@ -50,6 +52,7 @@
 import androidx.camera.video.internal.compat.quirk.CameraUseInconsistentTimebaseQuirk;
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
+import androidx.camera.video.internal.compat.quirk.MediaCodecDoesNotSendEos;
 import androidx.camera.video.internal.compat.quirk.VideoEncoderSuspendDoesNotIncludeSuspendTimeQuirk;
 import androidx.camera.video.internal.workaround.EncoderFinder;
 import androidx.camera.video.internal.workaround.VideoTimebaseConverter;
@@ -144,28 +147,29 @@
     private static final Range<Long> NO_RANGE = Range.create(NO_LIMIT_LONG, NO_LIMIT_LONG);
     private static final long STOP_TIMEOUT_MS = 1000L;
     private static final long TIMESTAMP_ANY = -1;
+    private static final int FAKE_BUFFER_INDEX = -9999;
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final String mTag;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final Object mLock = new Object();
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final boolean mIsVideoEncoder;
     private final MediaFormat mMediaFormat;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final MediaCodec mMediaCodec;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final EncoderInput mEncoderInput;
     private final EncoderInfo mEncoderInfo;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final Executor mEncoderExecutor;
     private final ListenableFuture<Void> mReleasedFuture;
     private final Completer<Void> mReleasedCompleter;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final Queue<Integer> mFreeInputBufferIndexQueue = new ArrayDeque<>();
     private final Queue<Completer<InputBuffer>> mAcquisitionQueue = new ArrayDeque<>();
     private final Set<InputBuffer> mInputBufferSet = new HashSet<>();
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
     /*
      * mActivePauseResumeTimeRanges is a queue used to track all active pause/resume time ranges.
@@ -173,31 +177,33 @@
      * range, so this range is still needed to check for later output buffers. The first element
      * in the queue is the oldest range and the last element is the newest.
      */
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     final Deque<Range<Long>> mActivePauseResumeTimeRanges = new ArrayDeque<>();
     final Timebase mInputTimebase;
     final TimeProvider mTimeProvider = new SystemTimeProvider();
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @GuardedBy("mLock")
     EncoderCallback mEncoderCallback = EncoderCallback.EMPTY;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @GuardedBy("mLock")
     Executor mEncoderCallbackExecutor = CameraXExecutors.directExecutor();
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     InternalState mState;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     Range<Long> mStartStopTimeRangeUs = NO_RANGE;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     long mTotalPausedDurationUs = 0L;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     boolean mPendingCodecStop = false;
     // The data timestamp that an encoding stops at. If this timestamp is null, it means the
     // encoding hasn't receiving enough data to be stopped.
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     Long mLastDataStopTimestamp = null;
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     Future<?> mStopTimeoutFuture = null;
+    @Nullable
+    private MediaCodecCallback mMediaCodecCallback;
 
     private boolean mIsFlushedAfterEndOfStream = false;
     private boolean mSourceStoppedSignalled = false;
@@ -276,7 +282,8 @@
             mStopTimeoutFuture.cancel(true);
             mStopTimeoutFuture = null;
         }
-        mMediaCodec.setCallback(new MediaCodecCallback());
+        mMediaCodecCallback = new MediaCodecCallback();
+        mMediaCodec.setCallback(mMediaCodecCallback);
         mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 
         if (mEncoderInput instanceof SurfaceInput) {
@@ -306,6 +313,7 @@
      * released. It can call {@link #pause} to pause the encoding after started. If the encoder is
      * in paused state, then calling this method will resume the encoding.
      */
+    @SuppressWarnings("StatementWithEmptyBody") // to better organize the logic and comments
     @Override
     public void start() {
         final long startTriggerTimeUs = generatePresentationTimeUs();
@@ -314,8 +322,7 @@
                 case CONFIGURED:
                     mLastDataStopTimestamp = null;
 
-                    final long startTimeUs = startTriggerTimeUs;
-                    Logger.d(mTag, "Start on " + DebugUtils.readableUs(startTimeUs));
+                    Logger.d(mTag, "Start on " + DebugUtils.readableUs(startTriggerTimeUs));
                     try {
                         if (mIsFlushedAfterEndOfStream) {
                             // If the codec is flushed after an end-of-stream, it was never
@@ -323,7 +330,7 @@
                             // before starting it again.
                             reset();
                         }
-                        mStartStopTimeRangeUs = Range.create(startTimeUs, NO_LIMIT_LONG);
+                        mStartStopTimeRangeUs = Range.create(startTriggerTimeUs, NO_LIMIT_LONG);
                         mMediaCodec.start();
                     } catch (MediaCodec.CodecException e) {
                         handleEncodeError(e);
@@ -345,14 +352,14 @@
                             pauseRange != null && pauseRange.getUpper() == NO_LIMIT_LONG,
                             "There should be a \"pause\" before \"resume\"");
                     final long pauseTimeUs = pauseRange.getLower();
-                    final long resumeTimeUs = startTriggerTimeUs;
-                    mActivePauseResumeTimeRanges.addLast(Range.create(pauseTimeUs, resumeTimeUs));
+                    mActivePauseResumeTimeRanges.addLast(
+                            Range.create(pauseTimeUs, startTriggerTimeUs));
                     // Do not update total paused duration here since current output buffer may
                     // still before the pause range.
 
-                    Logger.d(mTag, "Resume on " + DebugUtils.readableUs(resumeTimeUs)
+                    Logger.d(mTag, "Resume on " + DebugUtils.readableUs(startTriggerTimeUs)
                             + "\nPaused duration = " + DebugUtils.readableUs(
-                            (resumeTimeUs - pauseTimeUs))
+                            (startTriggerTimeUs - pauseTimeUs))
                     );
 
                     if (!mIsVideoEncoder && DeviceQuirks.get(
@@ -496,7 +503,7 @@
         });
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void signalCodecStop() {
         if (mEncoderInput instanceof ByteBufferInput) {
@@ -509,10 +516,15 @@
             Futures.successfulAsList(futures).addListener(this::signalEndOfInputStream,
                     mEncoderExecutor);
         } else if (mEncoderInput instanceof SurfaceInput) {
-            try {
-                mMediaCodec.signalEndOfInputStream();
-            } catch (MediaCodec.CodecException e) {
-                handleEncodeError(e);
+            if (DeviceQuirks.get(MediaCodecDoesNotSendEos.class) != null) {
+                requireNonNull(mMediaCodecCallback).onOutputBufferAvailable(mMediaCodec,
+                        FAKE_BUFFER_INDEX, createFakeEosBufferInfo());
+            } else {
+                try {
+                    mMediaCodec.signalEndOfInputStream();
+                } catch (MediaCodec.CodecException e) {
+                    handleEncodeError(e);
+                }
             }
         }
     }
@@ -540,9 +552,9 @@
                     break;
                 case STARTED:
                     // Create and insert a pause/resume range.
-                    final long pauseTimeUs = pauseTriggerTimeUs;
-                    Logger.d(mTag, "Pause on " + DebugUtils.readableUs(pauseTimeUs));
-                    mActivePauseResumeTimeRanges.addLast(Range.create(pauseTimeUs, NO_LIMIT_LONG));
+                    Logger.d(mTag, "Pause on " + DebugUtils.readableUs(pauseTriggerTimeUs));
+                    mActivePauseResumeTimeRanges.addLast(
+                            Range.create(pauseTriggerTimeUs, NO_LIMIT_LONG));
                     setState(PAUSED);
                     break;
                 case PENDING_RELEASE:
@@ -678,7 +690,7 @@
         mState = state;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void setMediaCodecPaused(boolean paused) {
         Bundle bundle = new Bundle();
@@ -686,7 +698,7 @@
         mMediaCodec.setParameters(bundle);
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void requestKeyFrameToMediaCodec() {
         Bundle bundle = new Bundle();
@@ -734,13 +746,13 @@
                 }, mEncoderExecutor);
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void handleEncodeError(@NonNull MediaCodec.CodecException e) {
         handleEncodeError(EncodeException.ERROR_CODEC, e.getMessage(), e);
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void handleEncodeError(@EncodeException.ErrorType int error, @Nullable String message,
             @Nullable Throwable throwable) {
@@ -760,6 +772,7 @@
                 stopMediaCodec(() -> notifyError(error, message, throwable));
                 break;
             case ERROR:
+                //noinspection ConstantConditions
                 Logger.w(mTag, "Get more than one error: " + message + "(" + error + ")",
                         throwable);
                 break;
@@ -769,7 +782,7 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     void notifyError(@EncodeException.ErrorType int error, @Nullable String message,
             @Nullable Throwable throwable) {
         EncoderCallback callback;
@@ -786,7 +799,7 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void stopMediaCodec(@Nullable Runnable afterStop) {
         /*
@@ -812,12 +825,12 @@
                 Logger.d(mTag, "encoded data and input buffers are returned");
             }
             if (mEncoderInput instanceof SurfaceInput && !mSourceStoppedSignalled) {
-                // For a SurfaceInput, the codec is in control of dequeuing buffers from the
-                // underlying BufferQueue. If we stop the codec, then it will stop dequeuing buffers
-                // and the BufferQueue may run out of input buffers, causing the camera pipeline
-                // to stall. Instead of stopping, we will flush the codec. Since the codec is
-                // operating in asynchronous mode, this will cause the codec to continue to
-                // discard buffers. We should have already received the end-of-stream signal on
+                // For a SurfaceInput, the codec is in control of de-queuing buffers from the
+                // underlying BufferQueue. If we stop the codec, then it will stop de-queuing
+                // buffers and the BufferQueue may run out of input buffers, causing the camera
+                // pipeline to stall. Instead of stopping, we will flush the codec. Since the
+                // codec is operating in asynchronous mode, this will cause the codec to continue
+                // to discard buffers. We should have already received the end-of-stream signal on
                 // an output buffer at this point, so those buffers are not needed anyways. We will
                 // defer resetting the codec until just before starting the codec again.
                 mMediaCodec.flush();
@@ -836,7 +849,7 @@
         }, mEncoderExecutor);
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void handleStopped() {
         if (mState == PENDING_RELEASE) {
@@ -859,7 +872,7 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void updateTotalPausedDuration(long bufferPresentationTimeUs) {
         while (!mActivePauseResumeTimeRanges.isEmpty()) {
@@ -876,7 +889,7 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     long getAdjustedTimeUs(@NonNull MediaCodec.BufferInfo bufferInfo) {
         long adjustedTimeUs;
@@ -888,7 +901,7 @@
         return adjustedTimeUs;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     boolean isInPauseRange(long timeUs) {
         for (Range<Long> range : mActivePauseResumeTimeRanges) {
@@ -903,7 +916,7 @@
         return false;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     @NonNull
     ListenableFuture<InputBuffer> acquireInputBuffer() {
@@ -940,12 +953,19 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @NonNull
+    private MediaCodec.BufferInfo createFakeEosBufferInfo() {
+        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+        bufferInfo.set(0, 0, generatePresentationTimeUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+        return bufferInfo;
+    }
+
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void matchAcquisitionsAndFreeBufferIndexes() {
         while (!mAcquisitionQueue.isEmpty() && !mFreeInputBufferIndexQueue.isEmpty()) {
-            Completer<InputBuffer> completer = mAcquisitionQueue.poll();
-            int bufferIndex = mFreeInputBufferIndexQueue.poll();
+            Completer<InputBuffer> completer = requireNonNull(mAcquisitionQueue.poll());
+            int bufferIndex = requireNonNull(mFreeInputBufferIndexQueue.poll());
 
             InputBufferImpl inputBuffer;
             try {
@@ -971,22 +991,22 @@
                 : new AudioEncoderInfoImpl(codecInfo, mime);
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     long generatePresentationTimeUs() {
         return mTimeProvider.uptimeUs();
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     static boolean isKeyFrame(@NonNull MediaCodec.BufferInfo bufferInfo) {
         return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     static boolean isEndOfStream(@NonNull MediaCodec.BufferInfo bufferInfo) {
         return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     class MediaCodecCallback extends MediaCodec.Callback {
         @Nullable
@@ -1101,11 +1121,14 @@
                                 return;
                             }
                         } else {
-                            try {
-                                mMediaCodec.releaseOutputBuffer(index, false);
-                            } catch (MediaCodec.CodecException e) {
-                                handleEncodeError(e);
-                                return;
+                            // Not necessary to return fake buffer
+                            if (index != FAKE_BUFFER_INDEX) {
+                                try {
+                                    mMediaCodec.releaseOutputBuffer(index, false);
+                                } catch (MediaCodec.CodecException e) {
+                                    handleEncodeError(e);
+                                    return;
+                                }
                             }
                         }
 
@@ -1252,6 +1275,7 @@
             return true;
         }
 
+        @SuppressWarnings("StatementWithEmptyBody") // to better organize the logic and comments
         @ExecutedBy("mEncoderExecutor")
         private boolean updatePauseRangeStateAndCheckIfBufferPaused(
                 @NonNull MediaCodec.BufferInfo bufferInfo) {
@@ -1372,7 +1396,7 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     class SurfaceInput implements Encoder.SurfaceInput {
 
@@ -1468,7 +1492,7 @@
         }
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    @SuppressWarnings("WeakerAccess") // synthetic accessor
     class ByteBufferInput implements Encoder.ByteBufferInput {
 
         private final Map<Observer<? super State>, Executor> mStateObservers =
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index b9f5851..d8cbb3e 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -41,6 +41,7 @@
 import android.hardware.display.DisplayManager;
 import android.media.MediaScannerConnection;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
@@ -154,12 +155,24 @@
  */
 public class CameraXActivity extends AppCompatActivity {
     private static final String TAG = "CameraXActivity";
-    private static final String[] REQUIRED_PERMISSIONS =
-            new String[]{
+    private static final String[] REQUIRED_PERMISSIONS;
+    static {
+        // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+        // granted any more.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            REQUIRED_PERMISSIONS = new String[]{
+                    Manifest.permission.CAMERA,
+                    Manifest.permission.RECORD_AUDIO
+            };
+        } else {
+            REQUIRED_PERMISSIONS = new String[]{
                     Manifest.permission.CAMERA,
                     Manifest.permission.RECORD_AUDIO,
                     Manifest.permission.WRITE_EXTERNAL_STORAGE
             };
+        }
+    }
+
     // Possible values for this intent key: "backward" or "forward".
     private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
     // Possible values for this intent key: "switch_test_case", "preview_test_case" or
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
index ad7a355..9cf35f4 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
@@ -29,10 +29,10 @@
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
 import androidx.test.uiautomator.UiDevice
 import java.util.concurrent.TimeUnit
 import org.junit.After
@@ -57,8 +57,7 @@
     )
 
     @get:Rule
-    val permissionRule =
-        GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
index e6096d7..7ade0d5 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
@@ -33,12 +33,12 @@
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
 import androidx.camera.testing.LabTestRule
 import androidx.lifecycle.Lifecycle
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
 import androidx.test.uiautomator.UiDevice
 import androidx.testutils.RepeatRule
 import androidx.testutils.withActivity
@@ -72,8 +72,7 @@
     )
 
     @get:Rule
-    val permissionRule =
-        GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
 
     @get:Rule
     val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
index 2a009f6..eb30443 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
@@ -31,7 +31,6 @@
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.StressTestRule
 import androidx.test.core.app.ApplicationProvider
@@ -40,6 +39,7 @@
 import androidx.test.espresso.matcher.ViewMatchers
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
 import androidx.test.uiautomator.UiDevice
 import androidx.testutils.RepeatRule
 import androidx.testutils.withActivity
@@ -70,8 +70,7 @@
     )
 
     @get:Rule
-    val permissionRule =
-        GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
 
     @get:Rule
     val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
index 7a832d46..12a63e2 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
@@ -32,12 +32,12 @@
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.StressTestRule
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
 import androidx.test.uiautomator.UiDevice
 import androidx.testutils.RepeatRule
 import androidx.testutils.withActivity
@@ -67,8 +67,7 @@
     )
 
     @get:Rule
-    val permissionRule =
-        GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
 
     @get:Rule
     val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
index 4218633..2ba7a62 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
@@ -32,7 +32,6 @@
 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.isCamera2ExtensionModeSupported
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.StressTestRule
 import androidx.lifecycle.Lifecycle
@@ -41,6 +40,7 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
 import androidx.test.uiautomator.UiDevice
 import org.junit.After
 import org.junit.Assume
@@ -70,8 +70,7 @@
     )
 
     @get:Rule
-    val permissionRule =
-        GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
 
     @get:Rule
     val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
index 1bba88b..7f63e72 100644
--- a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -17,10 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
     <uses-feature android:name="android.hardware.camera" />
-    <uses-permission
-        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
-        android:maxSdkVersion="32" />
-
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.CAMERA" />
 
     <application
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 5416004..85d64c1 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -27,6 +27,7 @@
 import static androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_DELETE_CAPTURED_IMAGE;
 import static androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_EXTENSION_MODE;
 
+import android.Manifest;
 import android.content.ContentValues;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
@@ -92,8 +93,10 @@
 import java.io.File;
 import java.text.Format;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
@@ -616,8 +619,31 @@
             Log.e(TAG, "Failed to obtain all required permissions.", exception);
             return new String[0];
         }
-        String[] permissions = info.requestedPermissions;
-        if (permissions != null && permissions.length > 0) {
+
+        if (info.requestedPermissions == null || info.requestedPermissions.length == 0) {
+            return new String[0];
+        }
+
+        List<String> requiredPermissions = new ArrayList<>();
+
+        // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+        // granted any more. When querying the requested permissions from PackageManager,
+        // READ_EXTERNAL_STORAGE will also be included if we specify WRITE_EXTERNAL_STORAGE
+        // requirement in AndroidManifest.xml. Therefore, also need to skip the permission check
+        // of READ_EXTERNAL_STORAGE.
+        for (String permission : info.requestedPermissions) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && (
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission)
+                            || Manifest.permission.READ_EXTERNAL_STORAGE.equals(permission))) {
+                continue;
+            }
+
+            requiredPermissions.add(permission);
+        }
+
+        String[] permissions = requiredPermissions.toArray(new String[0]);
+
+        if (permissions.length > 0) {
             return permissions;
         } else {
             return new String[0];
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
index 70a7ae4..ca918fdd 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
@@ -22,6 +22,7 @@
 import android.content.pm.ActivityInfo
 import android.content.pm.PackageManager
 import android.content.res.Configuration
+import android.os.Build
 import android.os.Bundle
 import android.os.Environment
 import android.os.Handler
@@ -264,12 +265,14 @@
             // Launch the permission request for CAMERA
             requestPermission.launch(Manifest.permission.CAMERA)
             return false
-        } else if (ContextCompat.checkSelfPermission(
+        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
+            ContextCompat.checkSelfPermission(
                 this,
                 Manifest.permission.WRITE_EXTERNAL_STORAGE
             ) != PackageManager.PERMISSION_GRANTED
         ) {
-            // Launch the permission request for WRITE_EXTERNAL_STORAGE
+            // Launch the permission request for WRITE_EXTERNAL_STORAGE. From Android T, skips to
+            // request WRITE_EXTERNAL_STORAGE permission since it won't be granted any more.
             requestPermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
             return false
         }
diff --git a/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml b/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml
index cc49044..c788afa 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
 
     <application
         android:allowBackup="true"
+        android:requestLegacyExternalStorage="true"
         android:label="@string/app_name"
         android:theme="@style/AppTheme">
 
diff --git a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
index faa2325..22a0b7c 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
+++ b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
@@ -222,12 +222,16 @@
             return
         }
 
-        val storagePermission = activity?.let {
-            ContextCompat.checkSelfPermission(it, Manifest.permission.WRITE_EXTERNAL_STORAGE)
-        }
-        if (storagePermission != PackageManager.PERMISSION_GRANTED) {
-            requestStoragePermission()
-            return
+        // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+        // granted any more.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            val storagePermission = activity?.let {
+                ContextCompat.checkSelfPermission(it, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            }
+            if (storagePermission != PackageManager.PERMISSION_GRANTED) {
+                requestStoragePermission()
+                return
+            }
         }
 
         sendSurfaceRequest(false)
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
index b164c35..eb5dc31 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
@@ -49,12 +49,23 @@
     private static final String PREVIEW_VIEW_FRAGMENT = "PreviewView";
     private static final String COMPOSE_UI_FRAGMENT = "ComposeUi";
 
-    private static final String[] REQUIRED_PERMISSIONS =
-            new String[]{
+    private static final String[] REQUIRED_PERMISSIONS;
+    static {
+        // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+        // granted any more.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            REQUIRED_PERMISSIONS = new String[]{
+                    Manifest.permission.CAMERA,
+                    Manifest.permission.RECORD_AUDIO
+            };
+        } else {
+            REQUIRED_PERMISSIONS = new String[]{
                     Manifest.permission.CAMERA,
                     Manifest.permission.RECORD_AUDIO,
                     Manifest.permission.WRITE_EXTERNAL_STORAGE
             };
+        }
+    }
     private static final int REQUEST_CODE_PERMISSIONS = 10;
 
     // Possible values for this intent key are the name values of LensFacing encoded as
diff --git a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
index 75681de..447d006 100644
--- a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
@@ -38,6 +38,7 @@
     method public final boolean onUnbind(android.content.Intent);
     field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
     field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
+    field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
     field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
     field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
     field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 75681de..447d006 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -38,6 +38,7 @@
     method public final boolean onUnbind(android.content.Intent);
     field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
     field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
+    field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
     field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
     field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
     field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 3da6251..4a8c423 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -122,6 +122,13 @@
     public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
 
     /**
+     * Used in the app manifest. It declares that this app declares physical objects with sensors,
+     * that connect and exchange data with other devices and systems.
+     */
+    @ExperimentalCarApi
+    public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
+
+    /**
      * Used to declare that this app is a settings app in the manifest. This app can be used to
      * provide screens corresponding to the settings page and/or any error resolution screens e.g.
      * sign-in screen.
diff --git a/compose/compiler/compiler-hosted/integration-tests/build.gradle b/compose/compiler/compiler-hosted/integration-tests/build.gradle
index 2086707..149e956 100644
--- a/compose/compiler/compiler-hosted/integration-tests/build.gradle
+++ b/compose/compiler/compiler-hosted/integration-tests/build.gradle
@@ -53,6 +53,7 @@
     testImplementation(libs.protobufLite)
     testImplementation(libs.guavaAndroid)
     testImplementation(project(":compose:compiler:compiler-hosted"))
+    testImplementation(projectOrArtifact(":compose:foundation:foundation"))
     testImplementation(projectOrArtifact(":compose:material:material"))
     testImplementation(project(":compose:runtime:runtime"))
     testImplementation(projectOrArtifact(":compose:ui:ui"))
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
index fa7fea9..ff87214 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
@@ -1073,7 +1073,7 @@
             }
             BasicText(AnnotatedString(
               text = "Some text"
-            ), null, null, null, <unsafe-coerce>(0), false, 0, null, %composer, 0, 0b11111110)
+            ), null, null, null, <unsafe-coerce>(0), false, 0, 0, null, %composer, 0, 0b000111111110)
             if (isTraceInProgress()) {
               traceEventEnd()
             }
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index d7dd537..15b1f37 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -888,13 +888,17 @@
   }
 
   public final class BasicTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
-    method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
   }
 
   public final class BasicTextKt {
-    method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
-    method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+    method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines);
+    method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent);
   }
 
   public final class ClickableTextKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index d23f77a..361a9e6 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -1132,13 +1132,17 @@
   }
 
   public final class BasicTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
-    method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
   }
 
   public final class BasicTextKt {
-    method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
-    method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+    method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines);
+    method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent);
   }
 
   public final class ClickableTextKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index d7dd537..15b1f37 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -888,13 +888,17 @@
   }
 
   public final class BasicTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
-    method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
   }
 
   public final class BasicTextKt {
-    method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
-    method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+    method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines);
+    method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
+    method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent);
   }
 
   public final class ClickableTextKt {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BasicTextMinMaxLinesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BasicTextMinMaxLinesDemo.kt
new file mode 100644
index 0000000..a4a4281
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BasicTextMinMaxLinesDemo.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.compose.foundation.demos.text
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun BasicTextMinMaxLinesDemo() {
+    Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+        TagLine("maxLines == line count")
+        TextWithMinMaxLines("Line 1\nLine 2", maxLines = 2)
+
+        TagLine("maxLines > line count")
+        TextWithMinMaxLines("abc", maxLines = 3)
+
+        TagLine("maxLines < line count")
+        TextWithMinMaxLines("Line 1\nLine 2\nLine 3", maxLines = 2)
+
+        TagLine("maxLines < line count with different line heights")
+        TextWithMinMaxLines(
+            text = buildAnnotatedString {
+                append("Line 1\n")
+                withStyle(SpanStyle(fontSize = fontSize8)) {
+                    append("Line 2\n")
+                }
+                append("Line 3")
+            },
+            maxLines = 2
+        )
+
+        TagLine("minLines == line count")
+        TextWithMinMaxLines("First line\nSecond line", minLines = 2)
+
+        TagLine("minLines < line count")
+        TextWithMinMaxLines("First line\nSecond line\nThird line", minLines = 2)
+
+        TagLine("minLines > line count")
+        var sameLineHeightsHasExtraLine by remember { mutableStateOf(false) }
+        val extraLine = if (sameLineHeightsHasExtraLine) "\nLine 4" else ""
+        TextWithMinMaxLines(
+            text = "Line 1\nLine 2\nLine 3$extraLine",
+            minLines = 4
+        )
+        Button(onClick = { sameLineHeightsHasExtraLine = !sameLineHeightsHasExtraLine }) {
+            Text(text = "Toggle last line")
+        }
+
+        TagLine("minLines > line count with different line heights")
+        var diffLineHeightsHasExtraLine by remember { mutableStateOf(false) }
+        TextWithMinMaxLines(
+            text = buildAnnotatedString {
+                append("Line 1\n")
+                withStyle(SpanStyle(fontSize = fontSize6)) {
+                    append("Line 2\n")
+                }
+                append("Line 3")
+                if (diffLineHeightsHasExtraLine) append("\nLine 4")
+            },
+            minLines = 4
+        )
+        Button(onClick = { diffLineHeightsHasExtraLine = !diffLineHeightsHasExtraLine }) {
+            Text(text = "Toggle last line")
+        }
+
+        TagLine("minLines < maxLines")
+        TextWithMinMaxLines(
+            "Line 1\nLine 2\nLine 3\nLine 4",
+            minLines = 2,
+            maxLines = 3
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun TextWithMinMaxLines(
+    text: AnnotatedString,
+    minLines: Int = 1,
+    maxLines: Int = Int.MAX_VALUE
+) {
+    BasicText(
+        text = text,
+        modifier = Modifier
+            .border(1.dp, Color.Gray)
+            .padding(2.dp),
+        maxLines = maxLines,
+        minLines = minLines
+    )
+}
+
+@Composable
+private fun TextWithMinMaxLines(
+    text: String = "",
+    minLines: Int = 1,
+    maxLines: Int = Int.MAX_VALUE
+) {
+    TextWithMinMaxLines(
+        text = AnnotatedString(text),
+        minLines = minLines,
+        maxLines = maxLines
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
index 4c56976..d760925 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.demos.text
 
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.runtime.Composable
@@ -24,8 +25,14 @@
 import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
 
 @Preview
 @Composable
@@ -33,36 +40,122 @@
     LazyColumn {
         item {
             TagLine("empty text, no maxLines")
-            TextFieldWithMaxLines("", maxLines = Int.MAX_VALUE)
+            TextFieldWithMinMaxLines("", maxLines = Int.MAX_VALUE)
         }
         item {
             TagLine("maxLines == line count")
-            TextFieldWithMaxLines("abc", maxLines = 1)
+            TextFieldWithMinMaxLines("abc", maxLines = 1)
         }
         item {
             TagLine("empty text, maxLines > line count")
-            TextFieldWithMaxLines("", maxLines = 2)
+            TextFieldWithMinMaxLines("", maxLines = 2)
         }
         item {
             TagLine("maxLines > line count")
-            TextFieldWithMaxLines("abc", maxLines = 4)
+            TextFieldWithMinMaxLines("abc", maxLines = 3)
         }
         item {
             TagLine("maxLines < line count")
-            TextFieldWithMaxLines("abc".repeat(20), maxLines = 1)
+            TextFieldWithMinMaxLines("abc".repeat(20), maxLines = 1)
+        }
+        item {
+            TagLine("empty text, no minLines")
+            TextFieldWithMinMaxLines("", minLines = 1)
+        }
+        item {
+            TagLine("minLines == line count")
+            TextFieldWithMinMaxLines(createMultilineText(2), minLines = 2)
+        }
+        item {
+            TagLine("empty text, minLines > line count")
+            TextFieldWithMinMaxLines("", minLines = 2)
+        }
+        item {
+            TagLine("minLines > line count")
+            TextFieldWithMinMaxLines(
+                createMultilineText(4),
+                minLines = 5
+            )
+        }
+        item {
+            TagLine("minLines < line count")
+            TextFieldWithMinMaxLines(createMultilineText(3), minLines = 2)
+        }
+        item {
+            TagLine("minLines < maxLines")
+            TextFieldWithMinMaxLines(
+                createMultilineText(4),
+                minLines = 2,
+                maxLines = 3
+            )
+        }
+        item {
+            TagLine("minLines == maxLines")
+            TextFieldWithMinMaxLines(
+                createMultilineText(2),
+                minLines = 3,
+                maxLines = 3
+            )
+        }
+        item {
+            TagLine("maxLines=4 with different line heights")
+            TextFieldWithMinMaxLines(
+                createMultilineText(5),
+                maxLines = 4,
+                spanStyles = listOf(
+                    AnnotatedString.Range(SpanStyle(fontSize = 40.sp), 14, 21)
+                )
+            )
+        }
+        item {
+            TagLine("minLines=5 with different line heights")
+            TextFieldWithMinMaxLines(
+                createMultilineText(4),
+                minLines = 5,
+                spanStyles = listOf(
+                    AnnotatedString.Range(SpanStyle(fontSize = 40.sp), 14, 21)
+                )
+            )
         }
     }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun TextFieldWithMaxLines(str: String? = null, maxLines: Int) {
+private fun TextFieldWithMinMaxLines(
+    str: String? = null,
+    minLines: Int = 1,
+    maxLines: Int = Int.MAX_VALUE,
+    spanStyles: List<AnnotatedString.Range<SpanStyle>>? = null
+) {
     val state = rememberSaveable { mutableStateOf(str ?: "abc ".repeat(20)) }
+
+    val visualTransformation: VisualTransformation =
+        if (spanStyles == null) {
+            VisualTransformation.None
+        } else {
+            VisualTransformation { annotatedString ->
+                TransformedText(
+                    AnnotatedString(
+                        annotatedString.text,
+                        spanStyles = spanStyles
+                    ),
+                    OffsetMapping.Identity
+                )
+            }
+        }
+
     BasicTextField(
         modifier = demoTextFieldModifiers.clipToBounds(),
         value = state.value,
         onValueChange = { state.value = it },
         textStyle = TextStyle(fontSize = fontSize8),
         cursorBrush = SolidColor(Color.Red),
-        maxLines = maxLines
+        minLines = minLines,
+        maxLines = maxLines,
+        visualTransformation = visualTransformation
     )
-}
\ No newline at end of file
+}
+
+private fun createMultilineText(lineCount: Int) =
+    (1..lineCount).joinToString("\n") { "Line $it" }
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index e539ce9..54610b2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -44,6 +44,7 @@
         ComposableDemo("Layout Reuse") { TextReuseLayoutDemo() },
         ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
         ComposableDemo("Interactive text") { InteractiveTextDemo() },
+        ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
         ComposableDemo("Ellipsize and letterspacing") { EllipsizeWithLetterSpacing() },
         ComposableDemo("Line breaking") { TextLineBreakingDemo() },
         DemoCategory(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index a4fdebf..d499986 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -52,7 +52,9 @@
  * [overflow] and TextAlign may have unexpected effects.
  * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  */
 @OptIn(InternalFoundationTextApi::class)
 @Composable
@@ -64,11 +66,16 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1
 ) {
     // NOTE(text-perf-review): consider precomputing layout here by pushing text to a channel...
     // something like:
     // remember(text) { precomputeTextLayout(text) }
-    require(maxLines > 0) { "maxLines should be greater than 0" }
+
+    // Unlike text field for which validation happens inside the 'heightInLines' modifier, in text
+    // 'maxLines' are not handled by the modifier but instead passed to the StaticLayout, therefore
+    // we perform validation here
+    validateMinMaxLines(minLines, maxLines)
 
     // selection registrar, if no SelectionContainer is added ambient value will be null
     val selectionRegistrar = LocalSelectionRegistrar.current
@@ -104,6 +111,7 @@
                     fontFamilyResolver = fontFamilyResolver,
                     overflow = overflow,
                     maxLines = maxLines,
+                    minLines = minLines,
                 ),
                 selectableId
             )
@@ -121,6 +129,7 @@
                 fontFamilyResolver = fontFamilyResolver,
                 overflow = overflow,
                 maxLines = maxLines,
+                minLines = minLines,
             )
         )
     }
@@ -130,7 +139,7 @@
         state.selectionBackgroundColor = LocalTextSelectionColors.current.backgroundColor
     }
 
-    Layout(modifier.then(controller.modifiers), controller.measurePolicy)
+    Layout(modifier = modifier.then(controller.modifiers), measurePolicy = controller.measurePolicy)
 }
 
 /**
@@ -151,7 +160,9 @@
  * [overflow] and TextAlign may have unexpected effects.
  * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param inlineContent A map store composables that replaces certain ranges of the text. It's
  * used to insert composables into text layout. Check [InlineTextContent] for more information.
  */
@@ -165,9 +176,13 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
-    inlineContent: Map<String, InlineTextContent> = mapOf(),
+    minLines: Int = 1,
+    inlineContent: Map<String, InlineTextContent> = mapOf()
 ) {
-    require(maxLines > 0) { "maxLines should be greater than 0" }
+    // Unlike text field for which validation happens inside the 'heightInLines' modifier, in text
+    // 'maxLines' are not handled by the modifier but instead passed to the StaticLayout, therefore
+    // we perform validation here
+    validateMinMaxLines(minLines, maxLines)
 
     // selection registrar, if no SelectionContainer is added ambient value will be null
     val selectionRegistrar = LocalSelectionRegistrar.current
@@ -244,10 +259,58 @@
     )
 }
 
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicText(
+    text: String,
+    modifier: Modifier = Modifier,
+    style: TextStyle = TextStyle.Default,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE
+) {
+    BasicText(
+        text = text,
+        modifier = modifier,
+        style = style,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        minLines = 1,
+        maxLines = maxLines
+    )
+}
+
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicText(
+    text: AnnotatedString,
+    modifier: Modifier = Modifier,
+    style: TextStyle = TextStyle.Default,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    inlineContent: Map<String, InlineTextContent> = mapOf(),
+) {
+    BasicText(
+        text = text,
+        modifier = modifier,
+        style = style,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        minLines = 1,
+        maxLines = maxLines,
+        inlineContent = inlineContent
+    )
+}
+
 /**
  * A custom saver that won't save if no selection is active.
  */
 private fun selectionIdSaver(selectionRegistrar: SelectionRegistrar?) = Saver<Long, Long>(
     save = { if (selectionRegistrar.hasSelection(it)) it else null },
     restore = { it }
-)
\ No newline at end of file
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 5ce4c33..e68d5b4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -94,11 +94,12 @@
  * [KeyboardOptions.imeAction].
  * @param singleLine when set to true, this text field becomes a single horizontally scrolling
  * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
- * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
- * maxLines attribute will be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * the return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are
+ * automatically set to 1.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param visualTransformation The visual transformation filter for changing the visual
  * representation of the input. By default no visual transformation is applied.
  * @param onTextLayout Callback that is executed when a new text layout is calculated. A
@@ -129,7 +130,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     visualTransformation: VisualTransformation = VisualTransformation.None,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -177,6 +179,7 @@
         imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
         keyboardActions = keyboardActions,
         softWrap = !singleLine,
+        minLines = if (singleLine) 1 else minLines,
         maxLines = if (singleLine) 1 else maxLines,
         decorationBox = decorationBox,
         enabled = enabled,
@@ -237,11 +240,12 @@
  * [KeyboardOptions.imeAction].
  * @param singleLine when set to true, this text field becomes a single horizontally scrolling
  * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
- * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
- * maxLines attribute will be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * the return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are
+ * automatically set to 1.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param visualTransformation The visual transformation filter for changing the visual
  * representation of the input. By default no visual transformation is applied.
  * @param onTextLayout Callback that is executed when a new text layout is calculated. A
@@ -272,7 +276,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     visualTransformation: VisualTransformation = VisualTransformation.None,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -296,9 +301,90 @@
         imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
         keyboardActions = keyboardActions,
         softWrap = !singleLine,
+        minLines = if (singleLine) 1 else minLines,
         maxLines = if (singleLine) 1 else maxLines,
         decorationBox = decorationBox,
         enabled = enabled,
         readOnly = readOnly
     )
 }
+
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicTextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = TextStyle.Default,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    cursorBrush: Brush = SolidColor(Color.Black),
+    decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+        @Composable { innerTextField -> innerTextField() }
+) {
+    BasicTextField(
+        value = value,
+        onValueChange = onValueChange,
+        modifier = modifier,
+        enabled = enabled,
+        readOnly = readOnly,
+        textStyle = textStyle,
+        keyboardOptions = keyboardOptions,
+        keyboardActions = keyboardActions,
+        singleLine = singleLine,
+        minLines = 1,
+        maxLines = maxLines,
+        visualTransformation = visualTransformation,
+        onTextLayout = onTextLayout,
+        interactionSource = interactionSource,
+        cursorBrush = cursorBrush,
+        decorationBox = decorationBox
+    )
+}
+
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicTextField(
+    value: TextFieldValue,
+    onValueChange: (TextFieldValue) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = TextStyle.Default,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    cursorBrush: Brush = SolidColor(Color.Black),
+    decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+        @Composable { innerTextField -> innerTextField() }
+) {
+    BasicTextField(
+        value = value,
+        onValueChange = onValueChange,
+        modifier = modifier,
+        enabled = enabled,
+        readOnly = readOnly,
+        textStyle = textStyle,
+        keyboardOptions = keyboardOptions,
+        keyboardActions = keyboardActions,
+        singleLine = singleLine,
+        minLines = 1,
+        maxLines = maxLines,
+        visualTransformation = visualTransformation,
+        onTextLayout = onTextLayout,
+        interactionSource = interactionSource,
+        cursorBrush = cursorBrush,
+        decorationBox = decorationBox
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 203924d..f9a41e9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -462,7 +462,7 @@
             }
             state.previousGlobalPosition = newGlobalPosition
         }
-    }
+    }.heightInLines(state.textDelegate.style, state.textDelegate.minLines)
 
     /*@VisibleForTesting*/
     internal var semanticsModifier = createSemanticsModifierFor(state.textDelegate.text)
@@ -560,6 +560,7 @@
     softWrap: Boolean = true,
     overflow: TextOverflow = TextOverflow.Clip,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = DefaultMinLines,
     placeholders: List<AnnotatedString.Range<Placeholder>>
 ): TextDelegate {
     // NOTE(text-perf-review): whenever we have remember intrinsic implemented, this might be a
@@ -569,6 +570,7 @@
         current.softWrap != softWrap ||
         current.overflow != overflow ||
         current.maxLines != maxLines ||
+        current.minLines != minLines ||
         current.density != density ||
         current.placeholders != placeholders ||
         current.fontFamilyResolver !== fontFamilyResolver
@@ -579,6 +581,7 @@
             softWrap = softWrap,
             overflow = overflow,
             maxLines = maxLines,
+            minLines = minLines,
             density = density,
             fontFamilyResolver = fontFamilyResolver,
             placeholders = placeholders,
@@ -598,6 +601,7 @@
     softWrap: Boolean = true,
     overflow: TextOverflow = TextOverflow.Clip,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = DefaultMinLines,
 ): TextDelegate {
     // NOTE(text-perf-review): whenever we have remember intrinsic implemented, this might be a
     // lot slower than the equivalent `remember(a, b, c, ...) { ... }` call.
@@ -606,6 +610,7 @@
         current.softWrap != softWrap ||
         current.overflow != overflow ||
         current.maxLines != maxLines ||
+        current.minLines != minLines ||
         current.density != density ||
         current.fontFamilyResolver !== fontFamilyResolver
     ) {
@@ -615,6 +620,7 @@
             softWrap = softWrap,
             overflow = overflow,
             maxLines = maxLines,
+            minLines = minLines,
             density = density,
             fontFamilyResolver = fontFamilyResolver,
         )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index cc57d5d..9bb0804 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -154,8 +154,10 @@
  * provided, there will be no cursor drawn
  * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
  * text will be positioned as if there was unlimited horizontal space.
- * @param maxLines The maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1.
+ * @param maxLines The maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param imeOptions Contains different IME configuration options.
  * @param keyboardActions when the input service emits an IME action, the corresponding callback
  * is called. Note that this IME action may be different from what you specified in
@@ -185,6 +187,7 @@
     cursorBrush: Brush = SolidColor(Color.Unspecified),
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = DefaultMinLines,
     imeOptions: ImeOptions = ImeOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     enabled: Boolean = true,
@@ -550,6 +553,7 @@
                 .heightIn(min = state.minHeightForSingleLineField)
                 .heightInLines(
                     textStyle = textStyle,
+                    minLines = minLines,
                     maxLines = maxLines
                 )
                 .textFieldScroll(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
index 2092f2d..7284b68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
@@ -34,8 +34,10 @@
 
 /**
  * The default minimum height in terms of minimum number of visible lines.
+ *
+ * Should not be used in public API and samples unless it's public, too.
  */
-internal const val DefaultMinLines: Int = 1
+internal const val DefaultMinLines = 1
 
 /**
  * Constraint the height of the text field so that it vertically occupies at least [minLines]
@@ -53,15 +55,7 @@
         properties["textStyle"] = textStyle
     }
 ) {
-    require(minLines > 0) {
-        "minLines must be greater than 0"
-    }
-    require(maxLines > 0) {
-        "maxLines must be greater than 0"
-    }
-    require(minLines <= maxLines) {
-        "minLines $minLines must be lower than or equal to maxLines $maxLines"
-    }
+    validateMinMaxLines(minLines, maxLines)
     if (minLines == DefaultMinLines && maxLines == Int.MAX_VALUE) return@composed Modifier
 
     val density = LocalDensity.current
@@ -126,4 +120,13 @@
             max = precomputedMaxLinesHeight?.toDp() ?: Dp.Unspecified
         )
     }
+}
+
+internal fun validateMinMaxLines(minLines: Int, maxLines: Int) {
+    require(minLines > 0 && maxLines > 0) {
+        "both minLines $minLines and maxLines $maxLines must be greater than zero"
+    }
+    require(minLines <= maxLines) {
+        "minLines $minLines must be less than or equal to maxLines $maxLines"
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
index 7a5b425..cdfbddf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
@@ -63,7 +63,10 @@
  *
  * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it is truncated such that subsequent
- * lines are dropped.
+ * lines are dropped. It is required that 1 <= [minLines] <= [maxLines].
+ *
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  *
  * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
  * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
@@ -86,6 +89,7 @@
     val text: AnnotatedString,
     val style: TextStyle,
     val maxLines: Int = Int.MAX_VALUE,
+    val minLines: Int = DefaultMinLines,
     val softWrap: Boolean = true,
     val overflow: TextOverflow = TextOverflow.Clip,
     val density: Density,
@@ -118,6 +122,8 @@
 
     init {
         check(maxLines > 0)
+        check(minLines > 0)
+        check(minLines <= maxLines)
     }
 
     fun layoutIntrinsics(layoutDirection: LayoutDirection) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
index 9ff9b59..e371923 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
 import com.google.common.truth.Truth.assertThat
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
@@ -31,10 +32,12 @@
     fun `semantics modifier recreated when TextDelegate is set`() {
         val textDelegateBefore = mock<TextDelegate>() {
             whenever(it.text).thenReturn(AnnotatedString("Example Text String 1"))
+            whenever(it.style).thenReturn(TextStyle.Default)
         }
 
         val textDelegateAfter = mock<TextDelegate>() {
             whenever(it.text).thenReturn(AnnotatedString("Example Text String 2"))
+            whenever(it.style).thenReturn(TextStyle.Default)
         }
 
         // Make sure that mock doesn't do smart memory management:
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
index d2ceef2..a878a1c 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
@@ -43,6 +43,7 @@
         )
 
         assertThat(textDelegate.maxLines).isEqualTo(Int.MAX_VALUE)
+        assertThat(textDelegate.minLines).isEqualTo(DefaultMinLines)
         assertThat(textDelegate.overflow).isEqualTo(TextOverflow.Clip)
     }
 
@@ -75,6 +76,21 @@
     }
 
     @Test
+    fun `constructor with customized minLines`() {
+        val minLines = 8
+
+        val textDelegate = TextDelegate(
+            text = AnnotatedString(text = ""),
+            style = TextStyle.Default,
+            minLines = minLines,
+            density = density,
+            fontFamilyResolver = fontFamilyResolver
+        )
+
+        assertThat(textDelegate.minLines).isEqualTo(minLines)
+    }
+
+    @Test
     fun `constructor with customized overflow`() {
         val overflow = TextOverflow.Ellipsis
 
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
index c8ef43b..ec1bd54 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
@@ -128,13 +128,13 @@
         )
     )
     BasicText(
-        text,
-        modifier,
-        mergedStyle,
-        onTextLayout,
-        overflow,
-        softWrap,
-        maxLines,
+        text = text,
+        modifier = modifier,
+        style = mergedStyle,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        maxLines = maxLines,
     )
 }
 
@@ -231,14 +231,14 @@
         )
     )
     BasicText(
-        text,
-        modifier,
-        mergedStyle,
-        onTextLayout,
-        overflow,
-        softWrap,
-        maxLines,
-        inlineContent
+        text = text,
+        modifier = modifier,
+        style = mergedStyle,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        maxLines = maxLines,
+        inlineContent = inlineContent
     )
 }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index c77e134..1705992 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -27,7 +27,6 @@
 import androidx.compose.animation.core.animateTo
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.rememberDraggableState
@@ -1163,11 +1162,10 @@
         Modifier
     }
 
-    Surface(modifier = modifier.then(appBarDragModifier)) {
+    Surface(modifier = modifier.then(appBarDragModifier), color = appBarContainerColor) {
         Column {
             TopAppBarLayout(
                 modifier = Modifier
-                    .background(color = appBarContainerColor)
                     .windowInsetsPadding(windowInsets)
                     // clip after padding so we don't show the title over the inset area
                     .clipToBounds(),
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
index f482132..060651b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
@@ -227,14 +227,14 @@
         )
     )
     BasicText(
-        text,
-        modifier,
-        mergedStyle,
-        onTextLayout,
-        overflow,
-        softWrap,
-        maxLines,
-        inlineContent
+        text = text,
+        modifier = modifier,
+        style = mergedStyle,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        maxLines = maxLines,
+        inlineContent = inlineContent
     )
 }
 
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
index 70b82b1..0f04167 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
@@ -17,14 +17,23 @@
 package androidx.compose.ui.text.platform
 
 import android.graphics.Typeface
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.platform.extensions.applySpanStyle
+import androidx.compose.ui.text.style.BaselineShift
 import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextGeometricTransform
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -44,6 +53,212 @@
         }
 
     @Test
+    fun fontSizeSp_shouldBeAppliedTo_textSize() {
+        val fontSize = 24.sp
+        val spanStyle = SpanStyle(fontSize = fontSize)
+        val tp = AndroidTextPaint(0, density.density)
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.textSize).isEqualTo(with(density) { fontSize.toPx() })
+        assertThat(notApplied.fontSize).isEqualTo(TextUnit.Unspecified)
+    }
+
+    @Test
+    fun fontSizeEm_shouldBeAppliedTo_textSize() {
+        val fontSize = 2.em
+        val spanStyle = SpanStyle(fontSize = fontSize)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.textSize = 30f
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.textSize).isEqualTo(60f)
+        assertThat(notApplied.fontSize).isEqualTo(TextUnit.Unspecified)
+    }
+
+    @Test
+    fun textGeometricTransform_shouldBeAppliedTo_scaleSkew() {
+        val textGeometricTransform = TextGeometricTransform(
+            scaleX = 1.5f,
+            skewX = 1f
+        )
+        val spanStyle = SpanStyle(textGeometricTransform = textGeometricTransform)
+        val tp = AndroidTextPaint(0, density.density)
+        val originalSkew = tp.textSkewX
+        val originalScale = tp.textScaleX
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.textSkewX).isEqualTo(originalSkew + textGeometricTransform.skewX)
+        assertThat(tp.textScaleX).isEqualTo(originalScale * textGeometricTransform.scaleX)
+        assertThat(notApplied.textGeometricTransform).isNull()
+    }
+
+    @Test
+    fun letterSpacingSp_shouldBeLeftAsSpan() {
+        val letterSpacing = 10.sp
+        val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.letterSpacing = 4f
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.letterSpacing).isEqualTo(4f)
+        assertThat(notApplied.letterSpacing).isEqualTo(letterSpacing)
+    }
+
+    @Test
+    fun letterSpacingEm_shouldBeAppliedTo_letterSpacing() {
+        val letterSpacing = 1.5.em
+        val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.letterSpacing = 4f
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.letterSpacing).isEqualTo(1.5f)
+        assertThat(notApplied.letterSpacing).isEqualTo(TextUnit.Unspecified)
+    }
+
+    @Test
+    fun letterSpacingUnspecified_shouldBeNoOp() {
+        val letterSpacing = TextUnit.Unspecified
+        val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.letterSpacing = 4f
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.letterSpacing).isEqualTo(4f)
+        assertThat(notApplied.letterSpacing).isEqualTo(TextUnit.Unspecified)
+    }
+
+    @Test
+    fun nonEmptyFontFeatureSettings_shouldBeAppliedTo_fontFeatureSettings() {
+        val fontFeatureSettings = "\"kern\" 0"
+        val spanStyle = SpanStyle(fontFeatureSettings = fontFeatureSettings)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.fontFeatureSettings = ""
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.fontFeatureSettings).isEqualTo(fontFeatureSettings)
+        assertThat(notApplied.fontFeatureSettings).isNull()
+    }
+
+    @Test
+    fun emptyFontFeatureSettings_shouldBeNotAppliedTo_fontFeatureSettings() {
+        val fontFeatureSettings = ""
+        val spanStyle = SpanStyle(fontFeatureSettings = fontFeatureSettings)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.fontFeatureSettings = "\"kern\" 0"
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.fontFeatureSettings).isEqualTo("\"kern\" 0")
+        assertThat(notApplied.fontFeatureSettings).isNull()
+    }
+
+    @Test
+    fun fontSettings_shouldBeAppliedTo_typeface() {
+        val fontFamily = FontFamily.Cursive
+        val fontWeight = FontWeight.W800
+        val fontStyle = FontStyle.Italic
+        val fontSynthesis = FontSynthesis.Style
+
+        val spanStyle = SpanStyle(
+            fontFamily = fontFamily,
+            fontWeight = fontWeight,
+            fontStyle = fontStyle,
+            fontSynthesis = fontSynthesis
+        )
+
+        val tp = AndroidTextPaint(0, density.density)
+        tp.typeface = Typeface.DEFAULT
+
+        var calledFontFamily: FontFamily? = null
+        var calledFontWeight: FontWeight? = null
+        var calledFontStyle: FontStyle? = null
+        var calledFontSynthesis: FontSynthesis? = null
+
+        val notApplied = tp.applySpanStyle(
+            spanStyle,
+            { family, weight, style, synthesis ->
+                calledFontFamily = family
+                calledFontWeight = weight
+                calledFontStyle = style
+                calledFontSynthesis = synthesis
+                Typeface.MONOSPACE
+            },
+            density
+        )
+
+        assertThat(tp.typeface).isEqualTo(Typeface.MONOSPACE)
+        assertThat(calledFontFamily).isEqualTo(fontFamily)
+        assertThat(calledFontWeight).isEqualTo(fontWeight)
+        assertThat(calledFontStyle).isEqualTo(fontStyle)
+        assertThat(calledFontSynthesis).isEqualTo(fontSynthesis)
+
+        assertThat(notApplied.fontFamily).isNull()
+        assertThat(notApplied.fontWeight).isNull()
+        assertThat(notApplied.fontStyle).isNull()
+        assertThat(notApplied.fontSynthesis).isNull()
+    }
+
+    @Test
+    fun baselineShift_shouldBeLeftAsSpan() {
+        val baselineShift = BaselineShift(0.8f)
+        val spanStyle = SpanStyle(baselineShift = baselineShift)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.baselineShift = 0
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.baselineShift).isEqualTo(0)
+        assertThat(notApplied.baselineShift).isEqualTo(baselineShift)
+    }
+
+    @Test
+    fun baselineShiftNone_shouldNotBeLeftAsSpan() {
+        val baselineShift = BaselineShift.None
+        val spanStyle = SpanStyle(baselineShift = baselineShift)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.baselineShift = 0
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.baselineShift).isEqualTo(0)
+        assertThat(notApplied.baselineShift).isNull()
+    }
+
+    @Test
+    fun background_shouldBeLeftAsSpan() {
+        val background = Color.Red
+        val spanStyle = SpanStyle(background = background)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.color = Color.Black.toArgb()
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.color).isEqualTo(Color.Black.toArgb())
+        assertThat(notApplied.background).isEqualTo(background)
+    }
+
+    @Test
+    fun backgroundTransparent_shouldNotBeLeftAsSpan() {
+        val background = Color.Transparent
+        val spanStyle = SpanStyle(background = background)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.color = Color.Black.toArgb()
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.color).isEqualTo(Color.Black.toArgb())
+        assertThat(notApplied.background).isEqualTo(Color.Unspecified)
+    }
+
+    @Test
     fun textDecorationUnderline_shouldBeLeftAsSpan() {
         val textDecoration = TextDecoration.Underline
         val spanStyle = SpanStyle(textDecoration = textDecoration)
@@ -83,4 +298,30 @@
         assertThat(tp.isStrikeThruText).isEqualTo(false)
         assertThat(notApplied.textDecoration).isNull()
     }
+
+    @Test
+    fun shadow_shouldBeAppliedTo_shadowLayer() {
+        val shadow = Shadow(Color.Red, Offset(4f, 4f), blurRadius = 8f)
+        val spanStyle = SpanStyle(shadow = shadow)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.clearShadowLayer()
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.shadow).isEqualTo(shadow)
+        assertThat(notApplied.shadow).isNull()
+    }
+
+    @Test
+    fun color_shouldBeAppliedTo_color() {
+        val color = Color.Red
+        val spanStyle = SpanStyle(color = color)
+        val tp = AndroidTextPaint(0, density.density)
+        tp.color = Color.Black.toArgb()
+
+        val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+        assertThat(tp.color).isEqualTo(Color.Red.toArgb())
+        assertThat(notApplied.color).isEqualTo(Color.Unspecified)
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
index 3b6a1cc..e7e664d 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
@@ -45,7 +45,9 @@
     }
 
     private var textDecoration: TextDecoration = TextDecoration.None
-    private var shadow: Shadow = Shadow.None
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal var shadow: Shadow = Shadow.None
 
     @VisibleForTesting
     internal var brush: Brush? = null
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index a2f23ee..38f9bc1 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -65,7 +65,7 @@
 }
 
 tasks.withType<KotlinCompile> {
-    kotlinOptions.jvmTarget = "17"
+    kotlinOptions.jvmTarget = "11"
 }
 
 // b/250726951 Gradle ProjectBuilder needs reflection access to java.lang.
@@ -82,4 +82,4 @@
     // some jars will be duplicate, we can pick any since they are
     // versioned.
     duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-}
\ No newline at end of file
+}
diff --git a/health/connect/connect-client-proto/src/main/proto/permission.proto b/health/connect/connect-client-proto/src/main/proto/permission.proto
index 97c4903..14fd285 100644
--- a/health/connect/connect-client-proto/src/main/proto/permission.proto
+++ b/health/connect/connect-client-proto/src/main/proto/permission.proto
@@ -28,7 +28,11 @@
   ACCESS_TYPE_WRITE = 2;
 }
 
+// Represents both the new and the old permission format.
+// If "permission" is set, the other 2 fields will be ignored.
 message Permission {
   optional DataType data_type = 1;
   optional AccessType access_type = 2;
+
+  optional string permission = 3;
 }
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index ca671b0..3578818 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -13,8 +13,9 @@
     method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.PermissionController getPermissionController();
     method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
-    method public default static boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
-    method public default static boolean isAvailable(android.content.Context context);
+    method public default static boolean isApiSupported();
+    method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static boolean isProviderAvailable(android.content.Context context);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
     method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -25,8 +26,9 @@
   public static final class HealthConnectClient.Companion {
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
-    method public boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
-    method public boolean isAvailable(android.content.Context context);
+    method public boolean isApiSupported();
+    method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public boolean isProviderAvailable(android.content.Context context);
   }
 
   public interface PermissionController {
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index ca671b0..3578818 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -13,8 +13,9 @@
     method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.PermissionController getPermissionController();
     method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
-    method public default static boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
-    method public default static boolean isAvailable(android.content.Context context);
+    method public default static boolean isApiSupported();
+    method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static boolean isProviderAvailable(android.content.Context context);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
     method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -25,8 +26,9 @@
   public static final class HealthConnectClient.Companion {
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
-    method public boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
-    method public boolean isAvailable(android.content.Context context);
+    method public boolean isApiSupported();
+    method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public boolean isProviderAvailable(android.content.Context context);
   }
 
   public interface PermissionController {
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 5965aa4..4acb5a6 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -13,8 +13,9 @@
     method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
     method public androidx.health.connect.client.PermissionController getPermissionController();
     method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
-    method public default static boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
-    method public default static boolean isAvailable(android.content.Context context);
+    method public default static boolean isApiSupported();
+    method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public default static boolean isProviderAvailable(android.content.Context context);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
     method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
     method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -25,8 +26,9 @@
   public static final class HealthConnectClient.Companion {
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
     method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
-    method public boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
-    method public boolean isAvailable(android.content.Context context);
+    method public boolean isApiSupported();
+    method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+    method public boolean isProviderAvailable(android.content.Context context);
   }
 
   public interface PermissionController {
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/AvailabilitySamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/AvailabilitySamples.kt
new file mode 100644
index 0000000..6bff47b
--- /dev/null
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/AvailabilitySamples.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.
+ */
+
+@file:Suppress("UNUSED_VARIABLE")
+
+package androidx.health.connect.client.samples
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.annotation.Sampled
+import androidx.health.connect.client.HealthConnectClient
+
+@Sampled
+suspend fun AvailabilityCheckSamples(context: Context, providerPackageName: String) {
+    if (!HealthConnectClient.isApiSupported()) {
+        return // early return as there is no viable integration
+    }
+    if (!HealthConnectClient.isProviderAvailable(context)) {
+        // Optionally redirect to package installer to find a provider, for example:
+        val uriString =
+            "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
+        context.startActivity(
+            Intent(Intent.ACTION_VIEW).apply {
+                setPackage("com.android.vending")
+                data = Uri.parse(uriString)
+                putExtra("overlay", true)
+                putExtra("callerId", context.packageName)
+            }
+        )
+        return
+    }
+    val healthConnectClient = HealthConnectClient.getOrCreate(context)
+    // Issue operations with healthConnectClient
+}
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IFilterGrantedPermissionsCallback.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IFilterGrantedPermissionsCallback.aidl
new file mode 100644
index 0000000..ce4a20c
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IFilterGrantedPermissionsCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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.health.platform.client.service;
+
+import androidx.health.platform.client.permission.Permission;
+import androidx.health.platform.client.error.ErrorStatus;
+
+oneway interface IFilterGrantedPermissionsCallback {
+  void onSuccess(in List<Permission> permissions) = 0;
+  void onError(in ErrorStatus status) = 1;
+}
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
index da62ba3..a3806cb 100644
--- a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
@@ -36,6 +36,7 @@
 import androidx.health.platform.client.service.IDeleteDataRangeCallback;
 import androidx.health.platform.client.service.IReadDataRangeCallback;
 import androidx.health.platform.client.service.IUpdateDataCallback;
+import androidx.health.platform.client.service.IFilterGrantedPermissionsCallback;
 import androidx.health.platform.client.service.IUpsertExerciseRouteCallback;
 import androidx.health.platform.client.service.IInsertDataCallback;
 import androidx.health.platform.client.service.IReadDataCallback;
@@ -50,11 +51,11 @@
    * API version of the AIDL interface. Should be incremented every time a new
    * method is added.
    */
-  const int CURRENT_API_VERSION = 3;
+  const int CURRENT_API_VERSION = 4;
 
   const int MIN_API_VERSION = 1;
 
-  // Next Id: 22
+  // Next Id: 23
 
   /**
    * Returns version of this AIDL interface.
@@ -66,6 +67,8 @@
 
   void getGrantedPermissions(in RequestContext context, in List<Permission> permissions, in IGetGrantedPermissionsCallback callback) = 3;
 
+  void filterGrantedPermissions(in RequestContext context, in List<Permission> permissions, in IFilterGrantedPermissionsCallback callback) = 22;
+
   void revokeAllPermissions(in RequestContext context, in IRevokeAllPermissionsCallback callback) = 8;
 
   void insertData(in RequestContext context, in UpsertDataRequest request, in IInsertDataCallback callback) = 9;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 4154168..77cc947 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -357,19 +357,40 @@
         internal const val DEFAULT_PROVIDER_PACKAGE_NAME = "com.google.android.apps.healthdata"
 
         /**
+         * Determines whether the current Health Connect SDK is supported on this device. If it is
+         * not supported, then installing any provider will not help - instead disable the
+         * integration.
+         *
+         * @return whether the api is supported on the device.
+         */
+        @JvmStatic
+        public fun isApiSupported(): Boolean {
+            if (!isSdkVersionSufficient()) {
+                return false
+            }
+            // We will redirect implementations on U or above, which isn't implemented yet.
+            // When developers guard their app with the isSupported check, we can safely know they
+            // will not use Jetpack code that is not upgraded to U.
+            return Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU
+        }
+
+        /**
          * Determines whether an implementation of [HealthConnectClient] is available on this device
-         * at the moment.
+         * at the moment. If none is available, apps may choose to redirect to package installers to
+         * find suitable providers.
+         *
+         * @sample androidx.health.connect.client.samples.AvailabilityCheckSamples
          *
          * @param providerPackageNames optional package provider to choose implementation from
          * @return whether the api is available
          */
         @JvmOverloads
         @JvmStatic
-        public fun isAvailable(
+        public fun isProviderAvailable(
             context: Context,
             providerPackageNames: List<String> = listOf(DEFAULT_PROVIDER_PACKAGE_NAME),
         ): Boolean {
-            if (!isSdkVersionSufficient()) {
+            if (!isApiSupported()) {
                 return false
             }
             return providerPackageNames.any { isPackageInstalled(context.packageManager, it) }
@@ -385,7 +406,7 @@
          * @throws UnsupportedOperationException if service not available due to SDK version too low
          * @throws IllegalStateException if service not available due to not installed
          *
-         * @see isAvailable
+         * @see isProviderAvailable
          */
         @JvmOverloads
         @JvmStatic
@@ -393,10 +414,10 @@
             context: Context,
             providerPackageNames: List<String> = listOf(DEFAULT_PROVIDER_PACKAGE_NAME),
         ): HealthConnectClient {
-            if (!isSdkVersionSufficient()) {
+            if (!isApiSupported()) {
                 throw UnsupportedOperationException("SDK version too low")
             }
-            if (!isAvailable(context, providerPackageNames)) {
+            if (!isProviderAvailable(context, providerPackageNames)) {
                 throw IllegalStateException("Service not available")
             }
             val enabledPackage =
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index a9def5a..d08c1f1 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -16,6 +16,7 @@
 package androidx.health.connect.client
 
 import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RestrictTo
 import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
 import androidx.health.connect.client.permission.HealthDataRequestPermissions
 import androidx.health.connect.client.permission.HealthPermission
@@ -37,6 +38,20 @@
     suspend fun getGrantedPermissions(permissions: Set<HealthPermission>): Set<HealthPermission>
 
     /**
+     * Filters and returns a subset of permissions granted by the user to the calling app, out of
+     * the input permissions set.
+     *
+     * @param permissions set of permissions to filter
+     * @return filtered set of granted permissions.
+     *
+     * @throws android.os.RemoteException For any IPC transportation failures.
+     * @throws java.io.IOException For any disk I/O issues.
+     * @throws IllegalStateException If service is not available.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+    suspend fun filterGrantedPermissions(permissions: Set<String>): Set<String>
+
+    /**
      * Revokes all previously granted [HealthPermission] by the user to the calling app.
      *
      * @throws android.os.RemoteException For any IPC transportation failures.
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index 5ed5ce1..cd206a5 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -51,6 +51,7 @@
 import androidx.health.platform.client.HealthDataAsyncClient
 import androidx.health.platform.client.impl.logger.Logger
 import androidx.health.platform.client.proto.DataProto
+import androidx.health.platform.client.proto.PermissionProto
 import androidx.health.platform.client.proto.RequestProto
 import kotlin.reflect.KClass
 import kotlinx.coroutines.guava.await
@@ -83,6 +84,25 @@
         return grantedPermissions
     }
 
+    override suspend fun filterGrantedPermissions(
+        permissions: Set<String>
+    ): Set<String> {
+        val grantedPermissions =
+            delegate
+                .filterGrantedPermissions(
+                    permissions.map {
+                        PermissionProto.Permission.newBuilder().setPermission(it).build() }
+                        .toSet())
+                .await()
+                .map { it.permission }
+                .toSet()
+        Logger.debug(
+            HEALTH_CONNECT_CLIENT_TAG,
+            "Granted ${grantedPermissions.size} out of ${permissions.size} permissions."
+        )
+        return grantedPermissions
+    }
+
     override suspend fun revokeAllPermissions() {
         delegate.revokeAllPermissions().await()
         Logger.debug(HEALTH_CONNECT_CLIENT_TAG, "Revoked all permissions.")
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt
index 08074e2..3f5ac3f 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt
@@ -38,6 +38,14 @@
         permissions: Set<PermissionProto.Permission>
     ): ListenableFuture<Set<PermissionProto.Permission>>
 
+    /**
+     * Returns a set of [Permission] granted by the user to this app, out of the input [Permission]
+     * set.
+     */
+    fun filterGrantedPermissions(
+        permissions: Set<PermissionProto.Permission>
+    ): ListenableFuture<Set<PermissionProto.Permission>>
+
     /** Allows an app to relinquish app permissions granted to itself by calling this method. */
     fun revokeAllPermissions(): ListenableFuture<Unit>
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/FilterGrantedPermissionsCallback.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/FilterGrantedPermissionsCallback.kt
new file mode 100644
index 0000000..805bcd5
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/FilterGrantedPermissionsCallback.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.health.platform.client.impl
+
+import androidx.health.platform.client.error.ErrorStatus
+import androidx.health.platform.client.impl.error.toException
+import androidx.health.platform.client.permission.Permission
+import androidx.health.platform.client.proto.PermissionProto
+import androidx.health.platform.client.service.IFilterGrantedPermissionsCallback
+import com.google.common.util.concurrent.SettableFuture
+
+/** Wrapper to convert [IFilterGrantedPermissionsCallback] to listenable futures. */
+internal class FilterGrantedPermissionsCallback(
+    private val resultFuture: SettableFuture<Set<PermissionProto.Permission>>,
+) : IFilterGrantedPermissionsCallback.Stub() {
+    override fun onSuccess(response: List<Permission>) {
+        resultFuture.set(response.map { it.proto }.toSet())
+    }
+
+    override fun onError(error: ErrorStatus) {
+        resultFuture.setException(error.toException())
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
index 60ac65a..599b99d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
@@ -44,6 +44,7 @@
 import androidx.health.platform.client.service.IHealthDataService
 import androidx.health.platform.client.service.IHealthDataService.MIN_API_VERSION
 import com.google.common.util.concurrent.ListenableFuture
+import kotlin.math.min
 
 /** An IPC backed HealthDataClient implementation. */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -89,6 +90,18 @@
         }
     }
 
+    override fun filterGrantedPermissions(
+        permissions: Set<PermissionProto.Permission>,
+    ): ListenableFuture<Set<PermissionProto.Permission>> {
+        return executeWithVersionCheck(min(MIN_API_VERSION, 4)) { service, resultFuture ->
+            service.filterGrantedPermissions(
+                getRequestContext(),
+                permissions.map { Permission(it) }.toList(),
+                FilterGrantedPermissionsCallback(resultFuture)
+            )
+        }
+    }
+
     override fun revokeAllPermissions(): ListenableFuture<Unit> {
         return executeWithVersionCheck(MIN_API_VERSION) { service, resultFuture ->
             service.revokeAllPermissions(
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index a6dd148..39574d6 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -50,7 +50,8 @@
     fun noBackingImplementation_unavailable() {
         val packageManager = context.packageManager
         Shadows.shadowOf(packageManager).removePackage(PROVIDER_PACKAGE_NAME)
-        assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isApiSupported()).isTrue()
+        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
             .isFalse()
         assertThrows(IllegalStateException::class.java) {
             HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
@@ -61,7 +62,7 @@
     @Config(sdk = [Build.VERSION_CODES.P])
     fun backingImplementation_notEnabled_unavailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, enabled = false)
-        assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
             .isFalse()
         assertThrows(IllegalStateException::class.java) {
             HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
@@ -72,7 +73,7 @@
     @Config(sdk = [Build.VERSION_CODES.P])
     fun backingImplementation_enabledNoService_unavailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, enabled = true)
-        assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
             .isFalse()
         assertThrows(IllegalStateException::class.java) {
             HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
@@ -84,14 +85,16 @@
     fun backingImplementation_enabled_isAvailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, enabled = true)
         installService(context, PROVIDER_PACKAGE_NAME)
-        assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME))).isTrue()
+        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+            .isTrue()
         HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
     }
 
     @Test
     @Config(sdk = [Build.VERSION_CODES.O_MR1])
     fun sdkVersionTooOld_unavailable() {
-        assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+        assertThat(HealthConnectClient.isApiSupported()).isFalse()
+        assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
             .isFalse()
         assertThrows(UnsupportedOperationException::class.java) {
             HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
index 08bfe2c..49bb23a 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
@@ -42,6 +42,7 @@
 import androidx.health.platform.client.service.IAggregateDataCallback
 import androidx.health.platform.client.service.IDeleteDataCallback
 import androidx.health.platform.client.service.IDeleteDataRangeCallback
+import androidx.health.platform.client.service.IFilterGrantedPermissionsCallback
 import androidx.health.platform.client.service.IGetChangesCallback
 import androidx.health.platform.client.service.IGetChangesTokenCallback
 import androidx.health.platform.client.service.IGetGrantedPermissionsCallback
@@ -60,7 +61,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 class FakeHealthDataService : IHealthDataService.Stub() {
     /** Change this state to control permission responses. Not thread safe */
-    val grantedPermissions: MutableSet<Permission> = mutableSetOf()
+    private val grantedPermissions: MutableSet<Permission> = mutableSetOf()
 
     /** State retaining last requested parameters. */
     var lastRequestContext: RequestContext? = null
@@ -108,6 +109,21 @@
         callback.onSuccess(granted)
     }
 
+    override fun filterGrantedPermissions(
+        context: RequestContext,
+        permissions: List<Permission>,
+        callback: IFilterGrantedPermissionsCallback,
+    ) {
+        lastRequestContext = context
+        errorCode?.let {
+            callback.onError(ErrorStatus.create(it, "" + it))
+            return@filterGrantedPermissions
+        }
+
+        val granted = permissions.filter { it in grantedPermissions }.toList()
+        callback.onSuccess(granted)
+    }
+
     override fun revokeAllPermissions(
         context: RequestContext,
         callback: IRevokeAllPermissionsCallback,
diff --git a/health/health-services-client/api/1.0.0-beta01.txt b/health/health-services-client/api/1.0.0-beta01.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/1.0.0-beta01.txt
+++ b/health/health-services-client/api/1.0.0-beta01.txt
@@ -85,11 +85,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
-  public final class VersionApiService extends android.app.Service {
-    ctor public VersionApiService();
-    method public android.os.IBinder? onBind(android.content.Intent? intent);
-  }
-
 }
 
 package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
   }
 
   public final class LocationData {
-    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
     method public double getAltitude();
     method public double getBearing();
     method public double getLatitude();
@@ -669,8 +664,8 @@
     property public final double bearing;
     property public final double latitude;
     property public final double longitude;
-    field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
-    field public static final double BEARING_UNAVAILABLE = -1.0;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
   }
 
   public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -85,11 +85,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
-  public final class VersionApiService extends android.app.Service {
-    ctor public VersionApiService();
-    method public android.os.IBinder? onBind(android.content.Intent? intent);
-  }
-
 }
 
 package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
   }
 
   public final class LocationData {
-    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
     method public double getAltitude();
     method public double getBearing();
     method public double getLatitude();
@@ -669,8 +664,8 @@
     property public final double bearing;
     property public final double latitude;
     property public final double longitude;
-    field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
-    field public static final double BEARING_UNAVAILABLE = -1.0;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
   }
 
   public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
@@ -85,11 +85,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
-  public final class VersionApiService extends android.app.Service {
-    ctor public VersionApiService();
-    method public android.os.IBinder? onBind(android.content.Intent? intent);
-  }
-
 }
 
 package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
   }
 
   public final class LocationData {
-    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
     method public double getAltitude();
     method public double getBearing();
     method public double getLatitude();
@@ -669,8 +664,8 @@
     property public final double bearing;
     property public final double latitude;
     property public final double longitude;
-    field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
-    field public static final double BEARING_UNAVAILABLE = -1.0;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
   }
 
   public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -85,11 +85,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
-  public final class VersionApiService extends android.app.Service {
-    ctor public VersionApiService();
-    method public android.os.IBinder? onBind(android.content.Intent? intent);
-  }
-
 }
 
 package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
   }
 
   public final class LocationData {
-    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
     method public double getAltitude();
     method public double getBearing();
     method public double getLatitude();
@@ -669,8 +664,8 @@
     property public final double bearing;
     property public final double latitude;
     property public final double longitude;
-    field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
-    field public static final double BEARING_UNAVAILABLE = -1.0;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
   }
 
   public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/restricted_1.0.0-beta01.txt b/health/health-services-client/api/restricted_1.0.0-beta01.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta01.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta01.txt
@@ -85,11 +85,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
-  public final class VersionApiService extends android.app.Service {
-    ctor public VersionApiService();
-    method public android.os.IBinder? onBind(android.content.Intent? intent);
-  }
-
 }
 
 package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
   }
 
   public final class LocationData {
-    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
     method public double getAltitude();
     method public double getBearing();
     method public double getLatitude();
@@ -669,8 +664,8 @@
     property public final double bearing;
     property public final double latitude;
     property public final double longitude;
-    field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
-    field public static final double BEARING_UNAVAILABLE = -1.0;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
   }
 
   public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -85,11 +85,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
-  public final class VersionApiService extends android.app.Service {
-    ctor public VersionApiService();
-    method public android.os.IBinder? onBind(android.content.Intent? intent);
-  }
-
 }
 
 package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
   }
 
   public final class LocationData {
-    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
     method public double getAltitude();
     method public double getBearing();
     method public double getLatitude();
@@ -669,8 +664,8 @@
     property public final double bearing;
     property public final double latitude;
     property public final double longitude;
-    field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
-    field public static final double BEARING_UNAVAILABLE = -1.0;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
   }
 
   public final class MeasureCapabilities {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt
index a497e64..423cebe 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt
@@ -91,7 +91,7 @@
      */
     public open fun onPermissionLost() {}
 
-    private inner class IPassiveListenerServiceWrapper : IPassiveListenerService.Stub() {
+    internal inner class IPassiveListenerServiceWrapper : IPassiveListenerService.Stub() {
 
         override fun onPassiveListenerEvent(event: PassiveListenerEvent) {
             val proto = event.proto
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt b/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt
index 48f6af7..91d58a2 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt
@@ -20,10 +20,13 @@
 import android.content.Intent
 import android.os.IBinder
 import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
 import androidx.health.services.client.impl.IVersionApiService
 import androidx.health.services.client.impl.IpcConstants
 
 /** Service that allows querying the canonical SDK version used to compile this app. */
+@RestrictTo(Scope.LIBRARY)
 public class VersionApiService : Service() {
 
     private val stub: VersionApiServiceStub = VersionApiServiceStub()
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
index 07c0479..9d188b9 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
@@ -391,9 +391,8 @@
         @FloatRange(from = -90.0, to = 90.0) latitude: Double,
         @FloatRange(from = -180.0, to = 180.0) longitude: Double,
         timeDurationFromBoot: Duration,
-        @FloatRange altitude: Double = LocationData.ALTITUDE_UNAVAILABLE,
-        @FloatRange(from = -1.0, to = 360.0, toInclusive = false) bearing: Double =
-            LocationData.BEARING_UNAVAILABLE,
+        altitude: Double = LocationData.ALTITUDE_UNAVAILABLE,
+        bearing: Double = LocationData.BEARING_UNAVAILABLE,
         accuracy: LocationAccuracy? = null
     ): SampleDataPoint<LocationData> {
         if (latitude !in -90.0..90.0) {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt
index c1a68a5..8537b1b 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt
@@ -27,12 +27,11 @@
     /** Longitude of location. Range from -180.0 to = 180.0. */
     @FloatRange(from = -180.0, to = 180.0) public val longitude: Double,
     /** Altitude of location in meters or [ALTITUDE_UNAVAILABLE] if not available. */
-    @FloatRange public val altitude: Double = ALTITUDE_UNAVAILABLE,
+    public val altitude: Double = ALTITUDE_UNAVAILABLE,
     /** Bearing in degrees within the range of [0.0 (inclusive), 360.0(exclusive)] or
      * [BEARING_UNAVAILABLE] if not available.
      */
-    @FloatRange(from = -1.0, to = 360.0, toInclusive = false) public val bearing: Double =
-        BEARING_UNAVAILABLE,
+    public val bearing: Double = BEARING_UNAVAILABLE,
 ) {
     init {
         if (latitude !in -90.0..90.0) {
@@ -114,10 +113,10 @@
         private const val BEARING_INDEX: Int = 3
 
         /** When using [DataType.LOCATION], the default value if altitude value is not available. */
-        public const val ALTITUDE_UNAVAILABLE: Double = Double.MAX_VALUE
+        public const val ALTITUDE_UNAVAILABLE: Double = Double.NaN
 
         /** When using [DataType.LOCATION], the default value if bearing value is not available. */
-        public const val BEARING_UNAVAILABLE: Double = -1.0
+        public const val BEARING_UNAVAILABLE: Double = Double.NaN
 
         internal fun fromDataProtoValue(proto: DataProto.Value): LocationData {
             require(proto.hasDoubleArrayVal())
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
new file mode 100644
index 0000000..a4479a2
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.services.client
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import androidx.health.services.client.data.ComparisonType
+import androidx.health.services.client.data.DataPointContainer
+import androidx.health.services.client.data.DataPoints
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
+import androidx.health.services.client.data.DataTypeCondition
+import androidx.health.services.client.data.HealthEvent
+import androidx.health.services.client.data.HealthEvent.Type.Companion.FALL_DETECTED
+import androidx.health.services.client.data.PassiveGoal
+import androidx.health.services.client.data.PassiveMonitoringUpdate
+import androidx.health.services.client.data.UserActivityInfo
+import androidx.health.services.client.data.UserActivityState.Companion.USER_ACTIVITY_PASSIVE
+import androidx.health.services.client.impl.IPassiveListenerService
+import androidx.health.services.client.impl.event.PassiveListenerEvent
+import androidx.health.services.client.impl.response.HealthEventResponse
+import androidx.health.services.client.impl.response.PassiveMonitoringGoalResponse
+import androidx.health.services.client.impl.response.PassiveMonitoringUpdateResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.Instant
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class PassiveListenerServiceTest {
+    private fun Int.duration() = Duration.ofSeconds(this.toLong())
+    private fun Int.instant() = Instant.ofEpochMilli(this.toLong())
+
+    private val context = ApplicationProvider.getApplicationContext<Application>()
+    private lateinit var service: FakeService
+    private lateinit var stub: IPassiveListenerService
+
+    private val connection: ServiceConnection = object : ServiceConnection {
+        override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
+            stub = IPassiveListenerService.Stub.asInterface(binder)
+        }
+
+        override fun onServiceDisconnected(componentName: ComponentName) {}
+    }
+
+    @Before
+    fun setUp() {
+        service = FakeService()
+
+        Shadows.shadowOf(context).setBindServiceCallsOnServiceConnectedDirectly(true)
+        Shadows.shadowOf(context)
+            .setComponentNameAndServiceForBindService(
+                ComponentName(context, FakeService::class.java),
+                service.IPassiveListenerServiceWrapper()
+            )
+    }
+
+    @Test
+    fun receivesDataPoints() {
+        context.bindService(
+            Intent(context, FakeService::class.java),
+            connection,
+            Context.BIND_AUTO_CREATE
+        )
+        val listenerEvent = PassiveListenerEvent.createPassiveUpdateResponse(
+            PassiveMonitoringUpdateResponse(
+                PassiveMonitoringUpdate(
+                    DataPointContainer(
+                        listOf(
+                            DataPoints.dailySteps(100, 10.duration(), 20.duration())
+                        )
+                    ),
+                    listOf()
+                )
+            )
+        )
+
+        stub.onPassiveListenerEvent(listenerEvent)
+
+        val dataPoint = service.dataPointsReceived!!.getData(STEPS_DAILY).first()
+        assertThat(dataPoint.value).isEqualTo(100)
+        assertThat(dataPoint.startDurationFromBoot).isEqualTo(10.duration())
+        assertThat(dataPoint.endDurationFromBoot).isEqualTo(20.duration())
+    }
+
+    @Test
+    fun receivesUserActivityState() {
+        context.bindService(
+            Intent(context, FakeService::class.java),
+            connection,
+            Context.BIND_AUTO_CREATE
+        )
+        val listenerEvent = PassiveListenerEvent.createPassiveUpdateResponse(
+            PassiveMonitoringUpdateResponse(
+                PassiveMonitoringUpdate(
+                    DataPointContainer(listOf()),
+                    listOf(UserActivityInfo(USER_ACTIVITY_PASSIVE, null, 42.instant()))
+                )
+            )
+        )
+
+        stub.onPassiveListenerEvent(listenerEvent)
+
+        assertThat(service.userActivityReceived!!.userActivityState)
+            .isEqualTo(USER_ACTIVITY_PASSIVE)
+        assertThat(service.userActivityReceived!!.exerciseInfo).isNull()
+        assertThat(service.userActivityReceived!!.stateChangeTime).isEqualTo(42.instant())
+    }
+
+    @Test
+    fun receivesGoalCompleted() {
+        context.bindService(
+            Intent(context, FakeService::class.java),
+            connection,
+            Context.BIND_AUTO_CREATE
+        )
+        val listenerEvent = PassiveListenerEvent.createPassiveGoalResponse(
+            PassiveMonitoringGoalResponse(
+                PassiveGoal(
+                    DataTypeCondition(
+                        STEPS_DAILY,
+                        100,
+                        ComparisonType.GREATER_THAN
+                    )
+                )
+            )
+        )
+
+        stub.onPassiveListenerEvent(listenerEvent)
+
+        assertThat(service.goalReceived!!.dataTypeCondition.dataType).isEqualTo(STEPS_DAILY)
+        assertThat(service.goalReceived!!.dataTypeCondition.threshold).isEqualTo(100)
+        assertThat(service.goalReceived!!.dataTypeCondition.comparisonType)
+            .isEqualTo(ComparisonType.GREATER_THAN)
+    }
+
+    @Test
+    fun receivesHealthEvent() {
+        context.bindService(
+            Intent(context, FakeService::class.java),
+            connection,
+            Context.BIND_AUTO_CREATE
+        )
+        val listenerEvent = PassiveListenerEvent.createHealthEventResponse(
+            HealthEventResponse(
+                HealthEvent(
+                    FALL_DETECTED,
+                    42.instant(),
+                    DataPointContainer(listOf(DataPoints.heartRate(42.0, 84.duration())))
+                )
+            )
+        )
+
+        stub.onPassiveListenerEvent(listenerEvent)
+
+        assertThat(service.healthEventReceived!!.type).isEqualTo(FALL_DETECTED)
+        assertThat(service.healthEventReceived!!.eventTime).isEqualTo(42.instant())
+        val hrDataPoint =
+            service.healthEventReceived!!.metrics.getData(DataType.HEART_RATE_BPM).first()
+        assertThat(hrDataPoint.value).isEqualTo(42.0)
+    }
+
+    @Test
+    fun isNotifiedWhenPermissionsAreLost() {
+        context.bindService(
+            Intent(context, FakeService::class.java),
+            connection,
+            Context.BIND_AUTO_CREATE
+        )
+
+        stub.onPassiveListenerEvent(PassiveListenerEvent.createPermissionLostResponse())
+
+        assertThat(service.permissionLostCount).isEqualTo(1)
+    }
+
+    class FakeService : PassiveListenerService() {
+        var dataPointsReceived: DataPointContainer? = null
+        var userActivityReceived: UserActivityInfo? = null
+        var goalReceived: PassiveGoal? = null
+        var healthEventReceived: HealthEvent? = null
+        var permissionLostCount = 0
+
+        override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
+            dataPointsReceived = dataPoints
+        }
+
+        override fun onUserActivityInfoReceived(info: UserActivityInfo) {
+            userActivityReceived = info
+        }
+
+        override fun onGoalCompleted(goal: PassiveGoal) {
+            goalReceived = goal
+        }
+
+        override fun onHealthEventReceived(event: HealthEvent) {
+            healthEventReceived = event
+        }
+
+        override fun onPermissionLost() {
+            permissionLostCount++
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraryversions.toml b/libraryversions.toml
index 66c401d..0bb99db 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -13,7 +13,7 @@
 BLUETOOTH = "1.0.0-alpha01"
 BROWSER = "1.5.0-alpha01"
 BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.2.0-rc01"
+CAMERA = "1.3.0-alpha01"
 CAMERA_PIPE = "1.0.0-alpha01"
 CARDVIEW = "1.1.0-alpha01"
 CAR_APP = "1.3.0-beta02"
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
index d8e59d1..5999737 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
@@ -32,6 +32,7 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Types
 import androidx.privacysandbox.tools.core.model.getOnlyService
 import com.google.devtools.ksp.processing.CodeGenerator
 import com.google.devtools.ksp.processing.Dependencies
@@ -112,13 +113,17 @@
                             add(getDelegateCallBlock(method))
                         }
                         val value = api.valueMap[method.returnType]
-                        if (value != null) {
-                            addStatement(
-                                "transactionCallback.onSuccess(%M(result))",
-                                value.toParcelableNameSpec()
-                            )
-                        } else {
-                            addStatement("transactionCallback.onSuccess(result)")
+                        when {
+                            value != null -> {
+                                addStatement(
+                                    "transactionCallback.onSuccess(%M(result))",
+                                    value.toParcelableNameSpec()
+                                )
+                            }
+                            method.returnType == Types.unit -> {
+                                addStatement("transactionCallback.onSuccess()")
+                            }
+                            else -> addStatement("transactionCallback.onSuccess(result)")
                         }
                     }
                     addControlFlow("catch (t: Throwable)") {
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 73bde7b..8b88411 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -49,6 +49,7 @@
                 "com/mysdk/IMySdk.java",
                 "com/mysdk/ICancellationSignal.java",
                 "com/mysdk/IStringTransactionCallback.java",
+                "com/mysdk/IUnitTransactionCallback.java",
                 "com/mysdk/AbstractSandboxedSdkProvider.kt",
                 "com/mysdk/MySdkStubDelegate.kt",
                 "com/mysdk/TransportCancellationCallback.kt",
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
index cf15f15..8a358cf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
@@ -9,6 +9,8 @@
 
     suspend fun handleRequest(request: Request): Response
 
+    suspend fun logRequest(request: Request)
+
     fun doMoreStuff()
 }
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
index 4b22ebc..84a9d3b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
@@ -1,5 +1,6 @@
 package com.mysdk
 
+import com.mysdk.RequestConverter
 import com.mysdk.RequestConverter.fromParcelable
 import com.mysdk.ResponseConverter.toParcelable
 import kotlin.Int
@@ -44,6 +45,21 @@
     transactionCallback.onCancellable(cancellationSignal)
   }
 
+  public override fun logRequest(request: ParcelableRequest,
+      transactionCallback: IUnitTransactionCallback): Unit {
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.logRequest(fromParcelable(request))
+        transactionCallback.onSuccess()
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(404, t.message)
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
   public override fun doMoreStuff(): Unit {
     delegate.doMoreStuff()
   }
diff --git a/privacysandbox/tools/tools-apigenerator/build.gradle b/privacysandbox/tools/tools-apigenerator/build.gradle
index 7c22295..04b0326 100644
--- a/privacysandbox/tools/tools-apigenerator/build.gradle
+++ b/privacysandbox/tools/tools-apigenerator/build.gradle
@@ -34,6 +34,7 @@
     implementation project(path: ':privacysandbox:tools:tools-core')
 
     testImplementation(project(":internal-testutils-truth"))
+    testImplementation(project(":privacysandbox:tools:tools-apipackager"))
     testImplementation(project(":privacysandbox:tools:tools-testing"))
     testImplementation(project(":room:room-compiler-processing-testing"))
     testImplementation(libs.kotlinCoroutinesCore)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
index 941a432..5c13a3f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
@@ -16,33 +16,18 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
+import androidx.privacysandbox.tools.apipackager.PrivacySandboxApiPackager
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
 import androidx.room.compiler.processing.util.Source
-import com.google.common.truth.Truth.assertThat
-import java.io.File
 import java.nio.file.Files.createTempDirectory
 import java.nio.file.Path
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-import kotlin.io.path.name
 
 fun compileIntoInterfaceDescriptorsJar(vararg sources: Source): Path {
-    val tempDir = createTempDirectory("compile").toFile().also { it.deleteOnExit() }
-
+    val tempDir = createTempDirectory("compile").also { it.toFile().deleteOnExit() }
     val result = assertCompiles(sources.toList())
-    val sdkInterfaceDescriptors = File(tempDir, "sdk-interface-descriptors.jar")
-    assertThat(sdkInterfaceDescriptors.createNewFile()).isTrue()
-
-    ZipOutputStream(sdkInterfaceDescriptors.outputStream()).use { zipOutputStream ->
-        result.outputClasspath.forEach { classPathDir ->
-            classPathDir.walk().filter(File::isFile).forEach { file ->
-                val zipEntry = ZipEntry(classPathDir.toPath().relativize(file.toPath()).name)
-                zipOutputStream.putNextEntry(zipEntry)
-                file.inputStream().copyTo(zipOutputStream)
-                zipOutputStream.closeEntry()
-            }
-        }
-    }
-
-    return sdkInterfaceDescriptors.toPath()
+    val sdkInterfaceDescriptors = tempDir.resolve("sdk-interface-descriptors.jar")
+    PrivacySandboxApiPackager().packageSdkDescriptors(
+        result.outputClasspath.first().toPath(), sdkInterfaceDescriptors
+    )
+    return sdkInterfaceDescriptors
 }
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
new file mode 100644
index 0000000..4bcd717
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * 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("kotlin")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    implementation(libs.kotlinStdlibJdk8)
+    implementation(libs.asm)
+    implementation(libs.asmCommons)
+
+    implementation project(path: ':privacysandbox:tools:tools')
+
+    testImplementation(project(":internal-testutils-truth"))
+    testImplementation(project(":privacysandbox:tools:tools-testing"))
+    testImplementation(project(":room:room-compiler-processing-testing"))
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
+}
+
+androidx {
+    name = "androidx.privacysandbox.tools:tools-apipackager"
+    type = LibraryType.OTHER_CODE_PROCESSOR
+    mavenGroup = LibraryGroups.PRIVACYSANDBOX_TOOLS
+    inceptionYear = "2022"
+    description = "Packages API descriptions from SDKs compiled with Privacy Sandbox annotations."
+}
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
new file mode 100644
index 0000000..0a1ad9f
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.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.tools.apipackager
+
+import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.PrivacySandboxValue
+import java.io.File
+import org.objectweb.asm.AnnotationVisitor
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+
+internal object AnnotationInspector {
+    private val privacySandboxAnnotations = setOf(
+        PrivacySandboxCallback::class,
+        PrivacySandboxService::class,
+        PrivacySandboxValue::class,
+    )
+
+    fun hasPrivacySandboxAnnotation(classFile: File): Boolean {
+        val reader = ClassReader(classFile.readBytes())
+        val annotationExtractor = AnnotationExtractor()
+        reader.accept(
+            annotationExtractor,
+            ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
+        )
+        return annotationExtractor.hasPrivacySandboxAnnotation
+    }
+
+    private class AnnotationExtractor : ClassVisitor(Opcodes.ASM9) {
+        private val privacySandboxAnnotationDescriptors =
+            privacySandboxAnnotations.map { Type.getDescriptor(it.java) }.toSet()
+        var hasPrivacySandboxAnnotation = false
+            private set
+
+        override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
+            if (!hasPrivacySandboxAnnotation) {
+                descriptor?.let {
+                    hasPrivacySandboxAnnotation = privacySandboxAnnotationDescriptors.contains(it)
+                }
+            }
+            return super.visitAnnotation(descriptor, visible)
+        }
+    }
+}
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
new file mode 100644
index 0000000..cb5f049
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.tools.apipackager
+
+import androidx.privacysandbox.tools.apipackager.AnnotationInspector.hasPrivacySandboxAnnotation
+import java.nio.file.Path
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+import kotlin.io.path.notExists
+
+class PrivacySandboxApiPackager {
+
+    /**
+     * Extracts API descriptors from SDKs annotated with Privacy Sandbox annotations.
+     *
+     * This function will output a file with SDK interface descriptors, which can later be used to
+     * generate the client-side sources for communicating with this SDK over IPC through the
+     * privacy sandbox.
+     *
+     * @param sdkClasspath path to the root directory that contains the SDK's compiled classes.
+     *      Non-class files will be safely ignored.
+     * @param sdkInterfaceDescriptorsOutput output path for SDK Interface descriptors file.
+     */
+    fun packageSdkDescriptors(
+        sdkClasspath: Path,
+        sdkInterfaceDescriptorsOutput: Path,
+    ) {
+        require(sdkClasspath.exists() && sdkClasspath.isDirectory()) {
+            "$sdkClasspath is not a valid classpath."
+        }
+        require(sdkInterfaceDescriptorsOutput.notExists()) {
+            "$sdkInterfaceDescriptorsOutput already exists."
+        }
+
+        val outputFile = sdkInterfaceDescriptorsOutput.toFile().also {
+            it.parentFile.mkdirs()
+            it.createNewFile()
+        }
+
+        ZipOutputStream(outputFile.outputStream()).use { zipOutputStream ->
+            sdkClasspath.toFile().walk()
+                .filter { it.extension == "class" }
+                .filter { hasPrivacySandboxAnnotation(it) }
+                .forEach { classFile ->
+                    val zipEntry = ZipEntry(sdkClasspath.relativize(classFile.toPath()).toString())
+                    zipOutputStream.putNextEntry(zipEntry)
+                    classFile.inputStream().copyTo(zipOutputStream)
+                    zipOutputStream.closeEntry()
+                }
+        }
+    }
+}
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
new file mode 100644
index 0000000..5469653
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.tools.apipackager
+
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.zip.ZipInputStream
+import kotlin.io.path.createFile
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PrivacySandboxApiPackagerTest {
+
+    @Test
+    fun validSdkClasspath_onlyAnnotatedClassesAreReturned() {
+        val packagedSdkClasspath = compileAndReturnUnzippedPackagedClasspath(
+            Source.kotlin(
+                "com/mysdk/TestSandboxSdk.kt", """
+                    |package com.mysdk
+                    |
+                    |import androidx.privacysandbox.tools.PrivacySandboxCallback
+                    |import androidx.privacysandbox.tools.PrivacySandboxService
+                    |import androidx.privacysandbox.tools.PrivacySandboxValue
+                    |
+                    |@PrivacySandboxService
+                    |interface MySdk
+                    |
+                    |@PrivacySandboxValue
+                    |data class Value(val id: Int)
+                    |
+                    |@PrivacySandboxCallback
+                    |interface MySdkCallback
+                    |
+                    |interface InterfaceThatShouldBeIgnored {
+                    |    val sdk: MySdk
+                    |}
+                    |
+                    |data class DataClassThatShouldBeIgnored(val id: Int)
+                """.trimMargin()
+            )
+        )
+
+        val relativeDescriptorPaths = packagedSdkClasspath
+            .walk()
+            .filter { it.isFile }
+            .map { packagedSdkClasspath.toPath().relativize(it.toPath()).toString() }
+            .toList()
+        assertThat(relativeDescriptorPaths).containsExactly(
+            "com/mysdk/MySdk.class",
+            "com/mysdk/MySdkCallback.class",
+            "com/mysdk/Value.class",
+        )
+    }
+
+    @Test
+    fun validSdkClasspath_packagedDescriptorsCanBeLinkedAgainst() {
+        val packagedSdkClasspath = compileAndReturnUnzippedPackagedClasspath(
+            Source.kotlin(
+                "com/mysdk/TestSandboxSdk.kt", """
+                    |package com.mysdk
+                    |
+                    |import androidx.privacysandbox.tools.PrivacySandboxCallback
+                    |import androidx.privacysandbox.tools.PrivacySandboxService
+                    |import androidx.privacysandbox.tools.PrivacySandboxValue
+                    |
+                    |@PrivacySandboxService
+                    |interface MySdk
+                    |
+                    |@PrivacySandboxValue
+                    |data class Value(val id: Int)
+                    |
+                    |@PrivacySandboxCallback
+                    |interface MySdkCallback
+                """.trimMargin()
+            )
+        )
+
+        val appSource =
+            Source.kotlin(
+                "com/exampleapp/App.kt", """
+                |package com.exampleapp
+                |
+                |import com.mysdk.MySdk
+                |import com.mysdk.Value
+                |import com.mysdk.MySdkCallback
+                |
+                |class App(
+                |    val sdk: MySdk,
+                |    val sdkValue: Value,
+                |    val callback: MySdkCallback,
+                |)
+            """.trimMargin()
+            )
+        assertThat(compileWithExtraClasspath(appSource, packagedSdkClasspath)).succeeds()
+    }
+
+    @Test
+    fun sdkClasspathDoesNotExist_throwException() {
+        val invalidClasspathFile = makeTestDirectory().resolve("dir_that_does_not_exist")
+        val validSdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+        assertThrows<IllegalArgumentException> {
+            PrivacySandboxApiPackager().packageSdkDescriptors(
+                invalidClasspathFile, validSdkDescriptor
+            )
+        }
+    }
+
+    @Test
+    fun sdkClasspathNotADirectory_throwException() {
+        val invalidClasspathFile = makeTestDirectory().resolve("invalid-file.txt").also {
+            it.createFile()
+        }
+        val validSdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+        assertThrows<IllegalArgumentException> {
+            PrivacySandboxApiPackager().packageSdkDescriptors(
+                invalidClasspathFile, validSdkDescriptor
+            )
+        }
+    }
+
+    @Test
+    fun outputAlreadyExists_throwException() {
+        val source = Source.kotlin(
+            "com/mysdk/Valid.kt", """
+            |package com.mysdk
+            |interface Valid
+        """.trimMargin()
+        )
+        val sdkClasspath = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+            .outputClasspath.first().toPath()
+        val descriptorPathThatAlreadyExists =
+            makeTestDirectory().resolve("sdk-descriptors.jar").also {
+                it.createFile()
+            }
+        assertThrows<IllegalArgumentException> {
+            PrivacySandboxApiPackager().packageSdkDescriptors(
+                sdkClasspath, descriptorPathThatAlreadyExists
+            )
+        }
+    }
+
+    /** Compiles the given source files and returns a classpath with the results. */
+    private fun compileAndReturnUnzippedPackagedClasspath(vararg sources: Source): File {
+        val result = compileAll(sources.toList(), includePrivacySandboxPlatformSources = false)
+        assertThat(result).succeeds()
+        assertThat(result.outputClasspath).hasSize(1)
+
+        val originalClasspath = result.outputClasspath.first().toPath()
+        val descriptors = makeTestDirectory().resolve("sdk-descriptors.jar")
+        PrivacySandboxApiPackager().packageSdkDescriptors(originalClasspath, descriptors)
+
+        val outputDir = makeTestDirectory().toFile()
+        ZipInputStream(descriptors.toFile().inputStream()).use { input ->
+            generateSequence { input.nextEntry }
+                .forEach {
+                    val file: File = outputDir.resolve(it.name)
+                    file.parentFile.mkdirs()
+                    file.createNewFile()
+                    input.copyTo(file.outputStream())
+                }
+        }
+        return outputDir
+    }
+
+    private fun makeTestDirectory(): Path {
+        return Files.createTempDirectory("test")
+            .also {
+                it.toFile().deleteOnExit()
+            }
+    }
+
+    private fun compileWithExtraClasspath(source: Source, extraClasspath: File):
+        TestCompilationResult {
+        return compileAll(
+            listOf(source),
+            extraClasspath = listOf(extraClasspath),
+            includePrivacySandboxPlatformSources = false
+        )
+    }
+}
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 0c15531..11df687 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -25,6 +25,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import java.io.File
 import java.nio.file.Files
 import javax.tools.Diagnostic
 
@@ -37,14 +38,20 @@
 
     fun compileAll(
         sources: List<Source>,
+        extraClasspath: List<File> = emptyList(),
+        includePrivacySandboxPlatformSources: Boolean = true,
         symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
         processorOptions: Map<String, String> = emptyMap()
     ): TestCompilationResult {
         val tempDir = Files.createTempDirectory("compile").toFile().also { it.deleteOnExit() }
+        val targetSources = if (includePrivacySandboxPlatformSources) {
+            sources + syntheticPrivacySandboxSources
+        } else sources
         return compile(
             tempDir,
             TestCompilationArguments(
-                sources = sources + syntheticPrivacySandboxSources,
+                sources = targetSources,
+                classpath = extraClasspath,
                 symbolProcessorProviders = symbolProcessorProviders,
                 processorOptions = processorOptions,
             )
diff --git a/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/PrivacySandboxInterface.kt b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/PrivacySandboxInterface.kt
new file mode 100644
index 0000000..281a4d0
--- /dev/null
+++ b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/PrivacySandboxInterface.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.tools
+
+/**
+ * Annotated interfaces used by the client to communicate with the SDK in the privacy sandbox.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+annotation class PrivacySandboxInterface
\ No newline at end of file
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle
new file mode 100644
index 0000000..75b2f84
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.build.api.artifact.SingleArtifact
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+}
+
+// This project can be removed once b/239659205 has landed and use only the
+// profile-verification-sample project.
+android {
+    buildTypes {
+        release {
+            // Minification and shrinking are disabled to avoid r8 removing unused methods and
+            // speed up build process.
+            minifyEnabled false
+            shrinkResources false
+        }
+    }
+
+    flavorDimensions = ["version"]
+    productFlavors {
+        v1 {
+            dimension "version"
+            versionCode 1
+        }
+        v2 {
+            dimension "version"
+            versionCode 2
+        }
+        v3 {
+            dimension "version"
+            versionCode 3
+        }
+    }
+
+    namespace "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(project(":profileinstaller:profileinstaller"))
+
+    // These projects are not used directly but added to baseline-prof.txt to increase number of
+    // methods, in order to have dex opt to run.
+    implementation(project(":core:core"))
+    implementation(project(":core:core-ktx"))
+}
+
+// Define a configuration that can be consumed, as this project is a provider of test apks for
+// profile verification integration test.
+configurations {
+    apkAssets {
+        canBeConsumed = true
+        canBeResolved = false
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'profileverification-apkAssets')
+            )
+        }
+    }
+}
+
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+androidComponents {
+    onVariants(selector().all().withBuildType("release"), { variant ->
+        artifacts {
+            apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+        }
+    })
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1c2eb27
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml
@@ -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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:testOnly="false"
+        android:allowBackup="false"
+        android:label="Profileinstaller verification sample no profile"
+        android:supportsRtl="true"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".SampleActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            android:exported="false"
+            tools:node="merge">
+            <meta-data
+                android:name="androidx.profileinstaller.ProfileInstallerInitializer"
+                tools:node="remove" />
+        </provider>
+
+    </application>
+</manifest>
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt
new file mode 100644
index 0000000..11d9e76
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt
@@ -0,0 +1,2 @@
+Landroidx/**;
+HSPLandroidx/**;->**(**)**
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt
new file mode 100644
index 0000000..b742486
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt
@@ -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.
+ */
+
+package androidx.profileinstaller.integration.profileverification.target.no_initializer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.profileinstaller.ProfileVerifier
+import java.util.concurrent.Executors
+
+class SampleActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Executors.newSingleThreadExecutor().submit {
+            val result = ProfileVerifier.writeProfileVerification(this)
+            runOnUiThread {
+                findViewById<TextView>(R.id.txtNotice).text = """
+                    Profile installed: ${result.profileInstallResultCode}
+                    Has reference profile: ${result.isCompiledWithProfile}
+                    Has current profile: ${result.hasProfileEnqueuedForCompilation()}
+                """.trimIndent()
+            }
+        }
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b0a97a6
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="22sp"
+        android:layout_gravity="center" />
+
+</FrameLayout>
+
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..1ade21e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?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="app_notice">profileinstaller verification app.</string>
+</resources>
diff --git a/profileinstaller/integration-tests/profile-verification-sample/build.gradle b/profileinstaller/integration-tests/profile-verification-sample/build.gradle
new file mode 100644
index 0000000..8d9c645
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/build.gradle
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.build.api.artifact.SingleArtifact
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+}
+android {
+    buildTypes {
+        release {
+            // Minification and shrinking are disabled to avoid r8 removing unused methods and
+            // speed up build process.
+            minifyEnabled false
+            shrinkResources false
+        }
+    }
+
+    flavorDimensions = ["version"]
+    productFlavors {
+        v1 {
+            dimension "version"
+            versionCode 1
+        }
+        v2 {
+            dimension "version"
+            versionCode 2
+        }
+        v3 {
+            dimension "version"
+            versionCode 3
+        }
+    }
+
+    namespace "androidx.profileinstaller.integration.profileverification.target"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(project(":profileinstaller:profileinstaller"))
+
+    // These projects are not used directly but added to baseline-prof.txt to increase number of
+    // methods, in order to have dex opt to run.
+    implementation(project(":core:core"))
+    implementation(project(":core:core-ktx"))
+}
+
+// Define a configuration that can be consumed, as this project is a provider of test apks for
+// profile verification integration test.
+configurations {
+    apkAssets {
+        canBeConsumed = true
+        canBeResolved = false
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'profileverification-apkAssets')
+            )
+        }
+    }
+}
+
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+androidComponents {
+    onVariants(selector().all().withBuildType("release"), { variant ->
+        artifacts {
+            apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+        }
+    })
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..26fa0b3
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml
@@ -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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="false"
+        android:label="Profileinstaller verification sample"
+        android:supportsRtl="true"
+        android:testOnly="false"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".SampleActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt b/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt
new file mode 100644
index 0000000..11d9e76
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt
@@ -0,0 +1,2 @@
+Landroidx/**;
+HSPLandroidx/**;->**(**)**
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt b/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt
new file mode 100644
index 0000000..fd1a1e1
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt
@@ -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.
+ */
+
+package androidx.profileinstaller.integration.profileverification.target
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.profileinstaller.ProfileVerifier
+import java.util.concurrent.Executors
+
+class SampleActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Executors.newSingleThreadExecutor().submit {
+            val result = ProfileVerifier.getCompilationStatusAsync().get()
+            runOnUiThread {
+                findViewById<TextView>(R.id.txtNotice).text = """
+                    Profile installed: ${result.profileInstallResultCode}
+                    Has reference profile: ${result.isCompiledWithProfile}
+                    Has current profile: ${result.hasProfileEnqueuedForCompilation()}
+                """.trimIndent()
+            }
+        }
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b0a97a6
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="22sp"
+        android:layout_gravity="center" />
+
+</FrameLayout>
+
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..1ade21e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?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="app_notice">profileinstaller verification app.</string>
+</resources>
diff --git a/profileinstaller/integration-tests/profile-verification/build.gradle b/profileinstaller/integration-tests/profile-verification/build.gradle
new file mode 100644
index 0000000..a25c998
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/build.gradle
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+// This task copies the apks provided by the `apkAssets` configuration and places them in the
+// assets folder. This allows a build time generation of the sample apps.
+def copyApkTaskProvider = tasks.register("copyApkAssets", Copy) {
+    description = "Copies the asset apks provided by profile-verification-sample projects"
+    dependsOn(configurations.getByName("apkAssets"))
+    from(configurations.getByName("apkAssets").incoming.artifactView {}.files)
+    into(layout.buildDirectory.dir("intermediates/apkAssets"))
+
+    // Note that the artifact directory included contain multiple output-metadata.json files built
+    // with the apks. Since we're not interested in those we can simply exclude duplicates.
+    duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 23
+    }
+    sourceSets.androidTest.assets.srcDir(copyApkTaskProvider)
+    namespace "androidx.profileinstaller.integration.profileverification"
+}
+
+// Define a configuration that can be resolved. This project is the consumer of test apks, i.e. it
+// contains the integration tests.
+configurations {
+    apkAssets {
+        canBeConsumed = false
+        canBeResolved = true
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'profileverification-apkAssets')
+            )
+        }
+    }
+}
+
+dependencies {
+    androidTestImplementation(project(":profileinstaller:profileinstaller"))
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testUiautomator)
+    androidTestImplementation(libs.testExtTruth)
+    apkAssets(project(":profileinstaller:integration-tests:profile-verification-sample"))
+    apkAssets(project(":profileinstaller:integration-tests:profile-verification-sample-no-initializer"))
+}
+
+// It makes sure that the apks are generated before the assets are packed.
+afterEvaluate {
+    tasks.named("generateDebugAndroidTestAssets").configure { it.dependsOn(copyApkTaskProvider) }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<manifest />
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof
new file mode 100644
index 0000000..dafbbbb
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof
Binary files differ
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm
new file mode 100644
index 0000000..d72fd91
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm
Binary files differ
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationOnUnsupportedApiVersions.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationOnUnsupportedApiVersions.kt
new file mode 100644
index 0000000..e7af5ff
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationOnUnsupportedApiVersions.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller.integration.profileverification
+
+import android.os.Build
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
+import androidx.test.filters.LargeTest
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+
+@LargeTest
+class ProfileVerificationOnUnsupportedApiVersions {
+
+    @Before
+    fun setUp() {
+
+        // This test runs only on selected api version currently unsupported by profile verifier
+        Assume.assumeTrue(
+            Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
+                Build.VERSION.SDK_INT == Build.VERSION_CODES.R
+        )
+
+        withPackageName(PACKAGE_NAME_WITH_INITIALIZER) { uninstall() }
+        withPackageName(PACKAGE_NAME_WITHOUT_INITIALIZER) { uninstall() }
+    }
+
+    @After
+    fun tearDown() {
+        withPackageName(PACKAGE_NAME_WITH_INITIALIZER) { uninstall() }
+        withPackageName(PACKAGE_NAME_WITHOUT_INITIALIZER) { uninstall() }
+    }
+
+    @Test
+    fun unsupportedApiWithInitializer() = withPackageName(PACKAGE_NAME_WITH_INITIALIZER) {
+        install(apkName = APK_WITH_INITIALIZER, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun unsupportedApiWithoutInitializer() = withPackageName(PACKAGE_NAME_WITHOUT_INITIALIZER) {
+        install(apkName = APK_WITHOUT_INITIALIZER, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+    }
+
+    companion object {
+        private const val PACKAGE_NAME_WITH_INITIALIZER =
+            "androidx.profileinstaller.integration.profileverification.target"
+        private const val PACKAGE_NAME_WITHOUT_INITIALIZER =
+            "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+        private const val ACTIVITY_NAME =
+            ".SampleActivity"
+
+        private const val APK_WITHOUT_INITIALIZER =
+            "profile-verification-sample-no-initializer-v1-release.apk"
+        private const val APK_WITH_INITIALIZER =
+            "profile-verification-sample-v1-release.apk"
+    }
+}
\ No newline at end of file
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt
new file mode 100644
index 0000000..190a008
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller.integration.profileverification
+
+import androidx.profileinstaller.ProfileVersion
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * This test uses the project "profileinstaller:integration-tests:profile-verification-sample". The
+ * release version of it has been embedded in the assets in 3 different versions with increasing
+ * versionCode to allow updating the app through pm command.
+ *
+ * The SampleActivity invoked displays the status of the reference profile install on the UI after
+ * the callback from {@link ProfileVerifier} returns. This test checks the status visualized to
+ * confirm if the reference profile has been installed.
+ *
+ * This test needs min sdk version `P` because it's first version to introduce support for dm file:
+ * https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/3368431/
+ */
+@SdkSuppress(
+    minSdkVersion = android.os.Build.VERSION_CODES.P,
+    maxSdkVersion = ProfileVersion.MAX_SUPPORTED_SDK
+)
+@LargeTest
+class ProfileVerificationTestWithProfileInstallerInitializer {
+
+    @Before
+    fun setUp() = withPackageName(PACKAGE_NAME) {
+        // Note that this test fails on emulator api 30 (b/251540646)
+        assumeTrue(!isApi30)
+        uninstall()
+    }
+
+    @After
+    fun tearDown() = withPackageName(PACKAGE_NAME) {
+        uninstall()
+    }
+
+    @Test
+    fun installNewApp() = withPackageName(PACKAGE_NAME) {
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun installNewAppAndWaitForCompilation() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start once to install profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Start again, should still be awaiting compilation
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Compile
+        compileCurrentProfile()
+
+        // Start again to check profile is compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+
+        // Profile should still be compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installAppWithReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile.
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun updateFromReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+
+        // Updates adding reference profile
+        install(apkName = V2_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun updateFromNoReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+
+        // Updates adding reference profile
+        install(apkName = V3_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun updateFromReferenceProfileToNoReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+
+        // Updates removing reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun installWithReferenceProfileThenUpdateNoProfileThenUpdateProfileAgain() =
+        withPackageName(PACKAGE_NAME) {
+
+            // Install with reference profile
+            install(apkName = V1_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(true)
+            }
+
+            // Updates removing reference profile
+            install(apkName = V2_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(AWAITING_COMPILATION)
+                hasReferenceProfile(false)
+                hasCurrentProfile(true)
+            }
+
+            // Reinstall with reference profile
+            install(apkName = V3_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(true)
+            }
+        }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.profileinstaller.integration.profileverification.target"
+        private const val ACTIVITY_NAME =
+            ".SampleActivity"
+
+        // Note that these version differ only for version code 1..3 to allow update
+        private const val V1_APK = "profile-verification-sample-v1-release.apk"
+        private const val V2_APK = "profile-verification-sample-v2-release.apk"
+        private const val V3_APK = "profile-verification-sample-v3-release.apk"
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt
new file mode 100644
index 0000000..c83ac77
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller.integration.profileverification
+
+import androidx.profileinstaller.ProfileVersion
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * This test uses the project
+ * "profileinstaller:integration-tests:profile-verification-sample-no-initializer". The release
+ * version of it has been embedded in the assets in 3 different versions with increasing
+ * versionCode to allow updating the app through pm command. In this test the
+ * ProfileInstallerInitializer has been disabled from the target app manifest.
+ *
+ * The SampleActivity invoked displays the status of the reference profile install on the UI after
+ * the callback from {@link ProfileVerifier} returns. This test checks the status visualized to
+ * confirm if the reference profile has been installed.
+ *
+ * This test needs min sdk version `P` because it's first version to introduce support for dm file:
+ * https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/3368431/
+ */
+@SdkSuppress(
+    minSdkVersion = android.os.Build.VERSION_CODES.P,
+    maxSdkVersion = ProfileVersion.MAX_SUPPORTED_SDK
+)
+@LargeTest
+class ProfileVerificationTestWithoutProfileInstallerInitializer {
+
+    @Before
+    fun setUp() = withPackageName(PACKAGE_NAME) {
+        // Note that this test fails on emulator api 30 (b/251540646)
+        assumeTrue(!isApi30)
+        uninstall()
+    }
+
+    @After
+    fun tearDown() = withPackageName(PACKAGE_NAME) {
+        uninstall()
+    }
+
+    @Test
+    fun installNewAppWithoutReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installNewAppAndWaitForCompilation() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start once to check there is no profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+        stop()
+
+        // Start again to check there is no profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+        stop()
+
+        // Install profile through broadcast receiver
+        broadcastProfileInstallAction()
+
+        // Start again to check there it's now awaiting compilation
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Start again to check there it's now awaiting compilation
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Compile
+        compileCurrentProfile()
+
+        // Start again to check profile is compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+
+        // Start again to check profile is compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installAppWithReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun updateFromNoReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+
+        // Updates adding reference profile
+        install(apkName = V3_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun updateFromReferenceProfileToNoReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+
+        // Updates removing reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installWithReferenceProfileThenUpdateNoProfileThenUpdateProfileAgain() =
+        withPackageName(PACKAGE_NAME) {
+
+            // Install with reference profile
+            install(apkName = V1_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(false)
+            }
+
+            // Updates removing reference profile
+            install(apkName = V2_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(NONE)
+                hasReferenceProfile(false)
+                hasCurrentProfile(false)
+            }
+
+            // Reinstall with reference profile
+            install(apkName = V3_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(false)
+            }
+        }
+
+    @Test
+    fun forceInstallCurrentProfileThroughBroadcastReceiver() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start and assess there is no profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+        stop()
+
+        // Force update through broadcast receiver
+        broadcastProfileInstallAction()
+
+        // Start and assess there is a current profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun forceInstallCurrentProfileThroughBroadcastReceiverAndUpdateWithReference() =
+        withPackageName(PACKAGE_NAME) {
+
+            // Install without reference profile, start and assess there is no profile
+            install(apkName = V1_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(NONE)
+                hasReferenceProfile(false)
+                hasCurrentProfile(false)
+            }
+            stop()
+
+            // Force update through ProfileInstallerReceiver
+            broadcastProfileInstallAction()
+
+            // Start again and assess there is a current profile now installed
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(AWAITING_COMPILATION)
+                hasReferenceProfile(false)
+                hasCurrentProfile(true)
+            }
+
+            // Update to v2 and assert that the current profile was uninstalled
+            install(apkName = V2_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(NONE)
+                hasReferenceProfile(false)
+                hasCurrentProfile(false)
+            }
+
+            // Update to v3 with reference profile and assess this is correctly recognized
+            install(apkName = V3_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(false)
+            }
+        }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+        private const val ACTIVITY_NAME =
+            ".SampleActivity"
+
+        // Note that these version differ only for version code 1..3 to allow update
+        private const val V1_APK = "profile-verification-sample-no-initializer-v1-release.apk"
+        private const val V2_APK = "profile-verification-sample-no-initializer-v2-release.apk"
+        private const val V3_APK = "profile-verification-sample-no-initializer-v3-release.apk"
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt
new file mode 100644
index 0000000..d289ce0
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller.integration.profileverification
+
+import android.os.Build
+import android.os.Environment
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import androidx.concurrent.futures.DirectExecutor
+import androidx.profileinstaller.DeviceProfileWriter
+import androidx.profileinstaller.ProfileInstaller
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.io.File
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+fun withPackageName(packageName: String, block: WithPackageBlock.() -> Unit) {
+    block(WithPackageBlock(packageName))
+}
+
+class WithPackageBlock internal constructor(private val packageName: String) {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val uiAutomation = instrumentation.uiAutomation
+    private val uiDevice = UiDevice.getInstance(instrumentation)
+
+    // `externalMediaDirs` is deprecated. Docs encourage to put media in MediaStore instead. Here
+    // we just need a folder that can be accessed by both app and shell user so should be fine.
+    @Suppress("deprecation")
+    private val dirUsableByAppAndShell by lazy {
+        when {
+            Build.VERSION.SDK_INT >= 29 -> {
+                // On Android Q+ we are using the media directory because that is
+                // the directory that the shell has access to. Context: b/181601156
+                // This is the same logic of Outputs#init to determine `dirUsableByAppAndShell`.
+                InstrumentationRegistry.getInstrumentation().context.externalMediaDirs.firstOrNull {
+                    Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
+                }
+            }
+            Build.VERSION.SDK_INT <= 22 -> {
+                // prior to API 23, shell didn't have access to externalCacheDir
+                InstrumentationRegistry.getInstrumentation().context.cacheDir
+            }
+            else -> InstrumentationRegistry.getInstrumentation().context.externalCacheDir
+        } ?: throw IllegalStateException("Unable to select a directory for writing files.")
+    }
+
+    fun uninstall() = executeCommand("pm uninstall $packageName")
+
+    fun install(apkName: String, withProfile: Boolean) {
+
+        // Contains all the clean up actions to perform at the end of the execution
+        val cleanUpBlocks = mutableListOf<() -> (Unit)>()
+
+        try {
+
+            // First writes in a temp file the apk from the assets
+            val tmpApkFile = File(dirUsableByAppAndShell, "tmp_$apkName").also { file ->
+                file.delete()
+                file.createNewFile()
+                file.deleteOnExit()
+                file.outputStream().use { instrumentation.context.assets.open(apkName).copyTo(it) }
+            }
+            cleanUpBlocks.add { tmpApkFile.delete() }
+
+            // Then moves it to a destination that can be used to install it
+            val destApkPath = "$TEMP_DIR/$apkName"
+            assertThat(executeCommand("mv ${tmpApkFile.absolutePath} $destApkPath")).isEmpty()
+            cleanUpBlocks.add { executeCommand("rm $destApkPath") }
+
+            // This mimes the behaviour of `adb install-multiple` using an install session.
+            // For reference:
+            // https://source.corp.google.com/android-internal/packages/modules/adb/client/adb_install.cpp
+
+            // Creates an install session
+            val installCreateOutput = executeCommand("pm install-create -t").first().trim()
+            val sessionId = REGEX_SESSION_ID
+                .find(installCreateOutput)
+                .throwIfNull("pm install session is invalid.")
+                .groups[1]
+                .throwIfNull("pm install session is invalid.")
+                .value
+                .toLong()
+
+            // Adds the base apk to the install session
+            val successBaseApk =
+                executeCommand("pm install-write $sessionId base.apk $TEMP_DIR/$apkName")
+                    .first()
+                    .trim()
+                    .startsWith("Success")
+            if (!successBaseApk) {
+                throw IllegalStateException("Could not add $apkName to install session $sessionId")
+            }
+
+            // Adds the base dm (profile) to the install session
+            if (withProfile) {
+
+                // Generates the profiles using device profile writer
+                val tmpProfileProfFile = File(dirUsableByAppAndShell, "tmp_profile.prof")
+                    .also {
+                        it.delete()
+                        it.createNewFile()
+                        it.deleteOnExit()
+                    }
+                cleanUpBlocks.add { tmpProfileProfFile.delete() }
+
+                val deviceProfileWriter = DeviceProfileWriter(
+                    instrumentation.context.assets,
+                    DirectExecutor.INSTANCE,
+                    EMPTY_DIAGNOSTICS,
+                    apkName,
+                    BASELINE_PROF,
+                    BASELINE_PROFM,
+                    tmpProfileProfFile
+                )
+                if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
+                    throw IllegalStateException(
+                        "The device does not allow profile installer aot writes"
+                    )
+                }
+                val success = deviceProfileWriter.read()
+                    .transcodeIfNeeded()
+                    .write()
+                if (!success) {
+                    throw IllegalStateException(
+                        "Profile was not installed correctly."
+                    )
+                }
+
+                // Compress the profile to generate the dex metadata file
+                val tmpDmFile = File(dirUsableByAppAndShell, "tmp_base.dm")
+                    .also {
+                        it.delete()
+                        it.createNewFile()
+                        it.deleteOnExit()
+                        it.outputStream().use { os ->
+                            ZipOutputStream(os).use {
+
+                                // One single zip entry named `primary.prof`
+                                it.putNextEntry(ZipEntry("primary.prof"))
+                                it.write(tmpProfileProfFile.readBytes())
+                            }
+                        }
+                    }
+                cleanUpBlocks.add { tmpDmFile.delete() }
+
+                // Then moves it to a destination that can be used to install it
+                val dmFilePath = "$TEMP_DIR/$DM_FILE_NAME"
+                executeCommand("mv ${tmpDmFile.absolutePath} $dmFilePath")
+                cleanUpBlocks.add { executeCommand("rm $dmFilePath") }
+
+                // Tries to install using pm install write
+                val successBaseDm =
+                    executeCommand("pm install-write $sessionId base.dm $dmFilePath")
+                        .first()
+                        .trim()
+                        .startsWith("Success")
+
+                if (!successBaseDm) {
+                    throw IllegalStateException(
+                        "Could not add $dmFilePath to install session $sessionId"
+                    )
+                }
+            }
+
+            // Commit the install transaction. Note that install-commit may not print any output
+            // if it fails.
+            val commitCommandOutput = executeCommand("pm install-commit $sessionId")
+            val firstLine = commitCommandOutput.firstOrNull()?.trim()
+            if (firstLine == null || firstLine != "Success") {
+                throw IllegalStateException(
+                    "pm install-commit failed: ${commitCommandOutput.joinToString("\n")}"
+                )
+            }
+        } finally {
+
+            // Runs all the clean up blocks. This will clean up also partial operations in case
+            // there is an issue during install
+            cleanUpBlocks.forEach { it() }
+        }
+    }
+
+    fun start(activityName: String) {
+        val error = executeCommand("am start -n $packageName/$activityName")
+            .any { it.startsWith("Error") }
+        assertThat(error).isFalse()
+        uiDevice.waitForIdle()
+    }
+
+    fun stop() {
+        val error = executeCommand("am force-stop $packageName")
+            .any { it.startsWith("Error") }
+        assertThat(error).isFalse()
+        uiDevice.waitForIdle()
+    }
+
+    fun compileCurrentProfile() {
+        val stdout = executeCommand("cmd package compile -f -m speed-profile $packageName")
+        val success = stdout.first().trim() == "Success"
+        assertWithMessage("Profile compilation failed: `$stdout`").that(success).isTrue()
+    }
+
+    fun broadcastProfileInstallAction() {
+        val result = broadcast(
+            packageName = packageName,
+            action = "androidx.profileinstaller.action.INSTALL_PROFILE",
+            receiverClass = "androidx.profileinstaller.ProfileInstallReceiver"
+        )
+        assertWithMessage("Profile install action broadcast failed with code.")
+            .that(result)
+            .isEqualTo(ProfileInstaller.RESULT_INSTALL_SUCCESS)
+    }
+
+    private fun broadcast(packageName: String, action: String, receiverClass: String) =
+        executeCommand("am broadcast -a $action $packageName/$receiverClass")
+            .first { it.contains("Broadcast completed: result=") }
+            .split("=")[1]
+            .trim()
+            .toInt()
+
+    private fun executeCommand(command: String): List<String> {
+        Log.d(TAG, "Executing command: `$command`")
+        return ParcelFileDescriptor
+            .AutoCloseInputStream(uiAutomation.executeShellCommand(command))
+            .bufferedReader()
+            .lineSequence()
+            .toList()
+    }
+
+    fun evaluateUI(block: AssertUiBlock.() -> Unit) {
+        val resourceId = "id/txtNotice"
+        val lines = uiDevice
+            .wait(Until.findObject(By.res("$packageName:$resourceId")), UI_TIMEOUT)
+            .text
+            .lines()
+            .map { it.split(":")[1].trim() }
+        assertThat(lines).hasSize(3)
+        block(AssertUiBlock(lines))
+    }
+
+    companion object {
+        private const val TAG = "TestManager"
+        private const val TEMP_DIR = "/data/local/tmp/"
+        private const val UI_TIMEOUT = 20000L
+        private const val BASELINE_PROF = "baseline.prof"
+        private const val BASELINE_PROFM = "baseline.profm"
+        private const val DM_FILE_NAME = "base.dm"
+        private val REGEX_SESSION_ID = """\[(\d+)\]""".toRegex()
+    }
+
+    class AssertUiBlock(private val lines: List<String>) {
+        fun profileInstalled(resultCode: Int) =
+            assertWithMessage("Unexpected profile verification result code")
+                .that(lines[0].toInt())
+                .isEqualTo(resultCode)
+
+        fun hasReferenceProfile(value: Boolean) =
+            assertWithMessage("Unexpected hasReferenceProfile value")
+                .that(lines[1].toBoolean())
+                .isEqualTo(value)
+
+        fun hasCurrentProfile(value: Boolean) =
+            assertWithMessage("Unexpected hasCurrentProfile value")
+                .that(lines[2].toBoolean())
+                .isEqualTo(value)
+    }
+}
+
+val isApi30 by lazy { Build.VERSION.SDK_INT == Build.VERSION_CODES.R }
+
+const val COMPILED = RESULT_CODE_COMPILED_WITH_PROFILE
+const val AWAITING_COMPILATION = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
+const val NONE = RESULT_CODE_NO_PROFILE
+
+private fun <T> T?.throwIfNull(message: String): T = this ?: throw Exception(message)
+
+private val EMPTY_DIAGNOSTICS: ProfileInstaller.DiagnosticsCallback =
+    object : ProfileInstaller.DiagnosticsCallback {
+        private val TAG = "ProfileVerifierDiagnosticsCallback"
+        override fun onDiagnosticReceived(code: Int, data: Any?) {
+            Log.d(TAG, "onDiagnosticReceived: $code")
+        }
+
+        override fun onResultReceived(code: Int, data: Any?) {
+            Log.d(TAG, "onResultReceived: $code")
+        }
+    }
diff --git a/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d4c1970
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest />
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -46,5 +46,24 @@
     ctor public ProfileInstallerInitializer.Result();
   }
 
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
 }
 
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -46,5 +46,24 @@
     ctor public ProfileInstallerInitializer.Result();
   }
 
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
 }
 
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -46,5 +46,24 @@
     ctor public ProfileInstallerInitializer.Result();
   }
 
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
 }
 
diff --git a/profileinstaller/profileinstaller/build.gradle b/profileinstaller/profileinstaller/build.gradle
index 06356df..4a26114 100644
--- a/profileinstaller/profileinstaller/build.gradle
+++ b/profileinstaller/profileinstaller/build.gradle
@@ -24,6 +24,8 @@
 dependencies {
     annotationProcessor(libs.nullaway)
     api("androidx.startup:startup-runtime:1.1.1")
+    api(libs.guavaListenableFuture)
+    implementation("androidx.concurrent:concurrent-futures:1.1.0")
     implementation("androidx.annotation:annotation:1.2.0")
     testImplementation(libs.junit)
     testImplementation(libs.truth)
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 0bcb376..f1465e1 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -286,8 +286,9 @@
     }
 
     private static @Nullable byte[] desiredVersion() {
-        // If SDK is pre-N, we don't want to do anything, so return null.
-        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
+        // If SDK is pre or post supported version, we don't want to do anything, so return null.
+        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
+                || Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
             return null;
         }
 
@@ -318,7 +319,8 @@
 
     private static boolean requiresMetadata() {
         // If SDK is pre-N, we don't want to do anything, so return null.
-        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
+        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
+                || Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
             return false;
         }
 
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index 6f103d4..357b96f 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -388,8 +388,9 @@
      * @param filesDir for noting successful installation
      * @param apkName The apk file name the profile is targeting
      * @param diagnostics The diagnostics callback to pass diagnostics to
+     * @return True whether the operation was successful, false otherwise
      */
-    private static void transcodeAndWrite(
+    private static boolean transcodeAndWrite(
             @NonNull AssetManager assets,
             @NonNull String packageName,
             @NonNull PackageInfo packageInfo,
@@ -400,7 +401,7 @@
     ) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
             result(executor, diagnostics, ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, null);
-            return;
+            return false;
         }
         File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);
 
@@ -408,7 +409,7 @@
                 diagnostics, apkName, PROFILE_SOURCE_LOCATION, PROFILE_META_LOCATION, curProfile);
 
         if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
-            return; /* nothing else to do here */
+            return false; /* nothing else to do here */
         }
 
         boolean success = deviceProfileWriter.read()
@@ -418,6 +419,7 @@
         if (success) {
             noteProfileWrittenFor(packageInfo, filesDir);
         }
+        return success;
     }
 
     /**
@@ -531,16 +533,23 @@
             packageInfo = packageManager.getPackageInfo(packageName, 0);
         } catch (PackageManager.NameNotFoundException e) {
             diagnostics.onResultReceived(RESULT_IO_EXCEPTION, e);
+
+            // Calls the verification. Note that in this case since the force install failed we
+            // don't need to report it to the ProfileVerifier.
+            ProfileVerifier.writeProfileVerification(context, false);
             return;
         }
         File filesDir = context.getFilesDir();
         if (forceWriteProfile
                 || !hasAlreadyWrittenProfileForThisInstall(packageInfo, filesDir, diagnostics)) {
             Log.d(TAG, "Installing profile for " + context.getPackageName());
-            transcodeAndWrite(assetManager, packageName, packageInfo, filesDir, apkName, executor,
-                    diagnostics);
+            boolean profileWritten = transcodeAndWrite(assetManager, packageName, packageInfo,
+                    filesDir, apkName, executor, diagnostics);
+            ProfileVerifier.writeProfileVerification(
+                    context, profileWritten && forceWriteProfile);
         } else {
             Log.d(TAG, "Skipping profile installation for " + context.getPackageName());
+            ProfileVerifier.writeProfileVerification(context, false);
         }
     }
 
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java
new file mode 100644
index 0000000..671e131
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller;
+
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+import androidx.concurrent.futures.ResolvableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Provides API to verify whether a compilation profile was installed with the app. This does not
+ * make a distinction between cloud or baseline profile. The output of
+ * {@link #getCompilationStatusAsync()}  allows to check if the app has been compiled with a
+ * compiled profile or whether there is a profile enqueued for compilation.
+ *
+ * If {@link ProfileInstallerInitializer} was disabled, it's necessary to manually trigger the
+ * method {@link #writeProfileVerification(Context)} or the {@link ListenableFuture} returned by
+ * {@link ProfileVerifier#getCompilationStatusAsync()} will hang or timeout.
+ *
+ * Note that {@link ProfileVerifier} requires {@link Build.VERSION_CODES#P} due to a permission
+ * issue: the reference profile folder is not accessible to pre api 28. When calling this api on
+ * unsupported api, {@link #getCompilationStatusAsync()} returns
+ * {@link CompilationStatus#RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION}.
+ */
+public final class ProfileVerifier {
+    private static final String REF_PROFILES_BASE_DIR = "/data/misc/profiles/ref/";
+    private static final String CUR_PROFILES_BASE_DIR = "/data/misc/profiles/cur/0/";
+    private static final String PROFILE_FILE_NAME = "primary.prof";
+    private static final String PROFILE_INSTALLED_CACHE_FILE_NAME = "profileInstalled";
+    private static final ResolvableFuture<CompilationStatus> sFuture = ResolvableFuture.create();
+    private static final Object SYNC_OBJ = new Object();
+    private static final String TAG = "ProfileVerifier";
+
+    @Nullable
+    private static CompilationStatus sCompilationStatus = null;
+
+    private ProfileVerifier() {
+    }
+
+    /**
+     * Caches the information on whether a reference profile exists for this app. This method
+     * performs IO operations and should not be executed on main thread. Note that this method
+     * should be called manually a few seconds after app startup if
+     * {@link  ProfileInstallerInitializer} has been disabled.
+     *
+     * @param context an instance of the {@link Context}.
+     * @return the {@link CompilationStatus} of the app profile. Note that this is the same
+     * {@link CompilationStatus} obtained through {@link #getCompilationStatusAsync()}.
+     */
+    @WorkerThread
+    @NonNull
+    public static CompilationStatus writeProfileVerification(@NonNull Context context
+    ) {
+        return writeProfileVerification(context, false);
+    }
+
+    /**
+     * Caches the information on whether a reference profile exists for this app. This method
+     * performs IO operations and should not be executed on main thread. This specific api is for
+     * internal usage of this package only. The flag {@code forceVerifyCurrentProfile} should
+     * be triggered only when installing from broadcast receiver to force a current profile
+     * verification.
+     *
+     * @param context                   an instance of the {@link Context}.
+     * @param forceVerifyCurrentProfile requests a force verification for current profile. This
+     *                                  should be used when installing profile through
+     *                                  {@link ProfileInstallReceiver}.
+     * @return the {@link CompilationStatus} of the app profile. Note that this is the same
+     * {@link CompilationStatus} obtained through {@link #getCompilationStatusAsync()}.
+     * @hide
+     */
+    @NonNull
+    @WorkerThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    static CompilationStatus writeProfileVerification(
+            @NonNull Context context,
+            boolean forceVerifyCurrentProfile
+    ) {
+
+        // `forceVerifyCurrentProfile` can force a verification for the current profile only.
+        // Current profile can be installed at any time through the ProfileInstallerReceiver so
+        // the cached result won't work.
+        if (!forceVerifyCurrentProfile && sCompilationStatus != null) {
+            return sCompilationStatus;
+        }
+
+        synchronized (SYNC_OBJ) {
+
+            if (!forceVerifyCurrentProfile && sCompilationStatus != null) {
+                return sCompilationStatus;
+            }
+
+            // ProfileVerifier supports only api 28 and above.
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
+                    || Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+                return setCompilationStatus(
+                        RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION,
+                        false,
+                        false
+                );
+            }
+
+            // Check reference profile file existence. Note that when updating from a version with
+            // profile to a version without profile, a new reference profile of size zero is
+            // created. This should be equivalent to no reference profile.
+            File referenceProfileFile = new File(
+                    new File(REF_PROFILES_BASE_DIR, context.getPackageName()), PROFILE_FILE_NAME);
+            long referenceProfileSize = referenceProfileFile.length();
+            boolean hasReferenceProfile =
+                    referenceProfileFile.exists() && referenceProfileSize > 0;
+
+            // Check current profile file existence
+            File currentProfileFile = new File(
+                    new File(CUR_PROFILES_BASE_DIR, context.getPackageName()), PROFILE_FILE_NAME);
+            long currentProfileSize = currentProfileFile.length();
+            boolean hasCurrentProfile =
+                    currentProfileFile.exists() && currentProfileSize > 0;
+
+            // Checks package last update time that will be used to determine whether the app
+            // has been updated.
+            long packageLastUpdateTime;
+            try {
+                packageLastUpdateTime = getPackageLastUpdateTime(context);
+            } catch (PackageManager.NameNotFoundException e) {
+                return setCompilationStatus(
+                        RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
+                        hasReferenceProfile,
+                        hasCurrentProfile
+                );
+            }
+
+            // Reads the current profile verification cache file
+            File cacheFile = new File(context.getFilesDir(), PROFILE_INSTALLED_CACHE_FILE_NAME);
+            Cache currentCache = null;
+            if (cacheFile.exists()) {
+                try {
+                    currentCache = Cache.readFromFile(cacheFile);
+                } catch (IOException e) {
+                    return setCompilationStatus(
+                            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
+                            hasReferenceProfile,
+                            hasCurrentProfile
+                    );
+                }
+            }
+
+            // Here it's calculated the result code, initially set to either the latest saved value
+            // or `no profile exists`
+            int resultCode;
+
+            // There are 2 profiles: reference and current. These 2 are handled differently.
+            // The reference profile can be installed only by package manager or app Store.
+            // This can be assessed only at first app start or app updates (i.e. when the package
+            // info last update has changed). After the first install a reference profile can be
+            // created as a result of bg dex opt.
+
+            // Check if this is a first start or an update or the previous profile was awaiting
+            // compilation.
+            if (currentCache == null
+                    || currentCache.mPackageLastUpdateTime != packageLastUpdateTime
+                    || currentCache.mResultCode
+                    == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
+
+                // If so, reevaluate if the app has a reference profile and whether a current
+                // profile has been installed (since this runs after profile installer).
+                if (hasReferenceProfile) {
+                    resultCode = RESULT_CODE_COMPILED_WITH_PROFILE;
+                } else if (hasCurrentProfile) {
+                    resultCode = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+                } else {
+                    resultCode = RESULT_CODE_NO_PROFILE;
+                }
+            } else {
+
+                // If not, utilize the cached result since the reference profile might be the result
+                // of a bg dex opt.
+                resultCode = currentCache.mResultCode;
+            }
+
+            // A current profile can be installed by the profile installer also through broadcast,
+            // therefore if this was a forced installation it can happen at anytime. the flag
+            // `forceVerifyCurrentProfile` can request a force verification for the current
+            // profile only.
+            if (forceVerifyCurrentProfile && hasCurrentProfile
+                    && resultCode != RESULT_CODE_COMPILED_WITH_PROFILE) {
+                resultCode = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+            }
+
+            // If a profile has just been compiled, verify if the size matches between reference
+            // and current matches.
+            if (currentCache != null
+                    && (currentCache.mResultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION)
+                    && resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
+
+                // If there is an issue with the profile compilation, the reference profile size
+                // might be smaller than the current profile installed by profileinstaller. Note
+                // that this is not 100% accurate and it may return the wrong information if the
+                // portion of current profile added to the installed current profile, when the
+                // user uses the app, is larger than the installed current profile itself.
+
+                // The size of the reference profile should be at least the same in current if
+                // the compilation worked. Otherwise something went wrong. Note that on some api
+                // levels the reference profile file may not be visible to the app, so size
+                // cannot be read.
+                if (referenceProfileSize < currentCache.mInstalledCurrentProfileSize) {
+                    resultCode = RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING;
+                }
+            }
+
+            // We now have a new verification result.
+            Cache newCache = new Cache(
+                    /* schema = */ Cache.SCHEMA,
+                    /* resultCode = */ resultCode,
+                    /* packageLastUpdateTime = */ packageLastUpdateTime,
+                    /* installedCurrentProfileSize = */ currentProfileSize
+            );
+
+            // At this point we can cache the result if there was no cache file or if the result has
+            // changed (for example due to a force install).
+            if (currentCache == null || !currentCache.equals(newCache)) {
+                try {
+                    newCache.writeOnFile(cacheFile);
+                } catch (IOException e) {
+                    resultCode =
+                            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE;
+                }
+            }
+
+            // Set and report the calculated value
+            return setCompilationStatus(resultCode, hasReferenceProfile, hasCurrentProfile);
+        }
+    }
+
+    private static CompilationStatus setCompilationStatus(
+            int resultCode,
+            boolean hasReferenceProfile,
+            boolean hasCurrentProfile
+    ) {
+        sCompilationStatus = new CompilationStatus(
+                /* resultCode = */ resultCode,
+                /* hasReferenceProfile */ hasReferenceProfile,
+                /* hasCurrentProfile */ hasCurrentProfile
+        );
+        sFuture.set(sCompilationStatus);
+        return sCompilationStatus;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static long getPackageLastUpdateTime(Context context)
+            throws PackageManager.NameNotFoundException {
+
+        // PackageManager#getPackageInfo(String, int) was deprecated in API 33.
+        PackageManager packageManager = context.getApplicationContext().getPackageManager();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            return Api33Impl.getPackageInfo(packageManager, context).lastUpdateTime;
+        } else {
+            return packageManager.getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
+        }
+    }
+
+    /**
+     * Returns a future containing the {@link CompilationStatus} of the app profile. The
+     * {@link CompilationStatus} can be used to determine whether a baseline or cloud profile is
+     * installed either through app store or package manager (reference profile) or profile
+     * installer (current profile), in order to tag performance metrics versions. In the first
+     * case a reference profile is immediately installed, i.e. a the app has been compiled with a
+     * profile. In the second case the profile is awaiting compilation that will happen at some
+     * point later in background.
+     *
+     * @return A future containing the {@link CompilationStatus}.
+     */
+    @NonNull
+    public static ListenableFuture<CompilationStatus> getCompilationStatusAsync() {
+        return sFuture;
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    static class Cache {
+        private static final int SCHEMA = 1;
+        final int mSchema;
+        final int mResultCode;
+        final long mPackageLastUpdateTime;
+        final long mInstalledCurrentProfileSize;
+
+        Cache(
+                int schema,
+                int resultCode,
+                long packageLastUpdateTime,
+                long installedCurrentProfileSize
+        ) {
+            mSchema = schema;
+            mResultCode = resultCode;
+            mPackageLastUpdateTime = packageLastUpdateTime;
+            mInstalledCurrentProfileSize = installedCurrentProfileSize;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || !(o instanceof Cache)) return false;
+            Cache cacheFile = (Cache) o;
+            return mResultCode == cacheFile.mResultCode
+                    && mPackageLastUpdateTime == cacheFile.mPackageLastUpdateTime
+                    && mSchema == cacheFile.mSchema
+                    && mInstalledCurrentProfileSize == cacheFile.mInstalledCurrentProfileSize;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(
+                    mResultCode,
+                    mPackageLastUpdateTime,
+                    mSchema,
+                    mInstalledCurrentProfileSize
+            );
+        }
+
+        void writeOnFile(@NonNull File file) throws IOException {
+            file.delete();
+            try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
+                dos.writeInt(mSchema);
+                dos.writeInt(mResultCode);
+                dos.writeLong(mPackageLastUpdateTime);
+                dos.writeLong(mInstalledCurrentProfileSize);
+            }
+        }
+
+        static Cache readFromFile(@NonNull File file) throws IOException {
+            try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
+                return new Cache(
+                        dis.readInt(),
+                        dis.readInt(),
+                        dis.readLong(),
+                        dis.readLong()
+                );
+            }
+        }
+    }
+
+    /**
+     * {@link CompilationStatus} contains the result of a profile verification operation. It
+     * offers API to determine whether a profile was installed
+     * {@link CompilationStatus#getProfileInstallResultCode()} and to check whether the app has
+     * been compiled with a profile or a profile is enqueued for compilation. Note that the
+     * app can be compiled with a profile also as result of background dex optimization.
+     */
+    public static class CompilationStatus {
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+                RESULT_CODE_NO_PROFILE,
+                RESULT_CODE_COMPILED_WITH_PROFILE,
+                RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION,
+                RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING,
+                RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
+                RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
+                RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
+                RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
+        })
+        public @interface ResultCode {
+        }
+
+        private static final int RESULT_CODE_ERROR_CODE_BIT_SHIFT = 16;
+
+        /**
+         * Indicates that no profile was installed for this app. This means that no profile was
+         * installed when installing the app through app store or package manager and profile
+         * installer either didn't run ({@link ProfileInstallerInitializer} disabled) or the app
+         * was packaged without a compilation profile.
+         */
+        public static final int RESULT_CODE_NO_PROFILE = 0;
+
+        /**
+         * Indicates that a profile is installed and the app has been compiled with it. This is the
+         * result of installation through app store or package manager, or installation through
+         * profile installer and subsequent compilation during background dex optimization.
+         */
+        public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1;
+
+        /**
+         * Indicates that a profile is installed and the app will be compiled with it later when
+         * background dex optimization runs. This is the result of installation through profile
+         * installer. When the profile is compiled, the result code will change to
+         * {@link #RESULT_CODE_COMPILED_WITH_PROFILE}.
+         */
+        public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2;
+
+        /**
+         * Indicates that a profile is installed and the app has been compiled with it.
+         * This is the result of installation through app store or package manager. Note that
+         * this result differs from {@link #RESULT_CODE_COMPILED_WITH_PROFILE} as the profile
+         * is smaller than expected and may not include all the methods initially included in the
+         * baseline profile.
+         */
+        public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3;
+
+        /**
+         * Indicates an error during the verification process: a
+         * {@link PackageManager.NameNotFoundException} was launched when querying the
+         * {@link PackageManager} for the app package.
+         */
+        public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST =
+                1 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        /**
+         * Indicates that a previous verification result cache file exists but it cannot be read.
+         */
+        public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ =
+                2 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        /**
+         * Indicates that wasn't possible to write the verification result cache file. This can
+         * happen only because something is wrong with app folder permissions or if there is no
+         * free disk space on the device.
+         */
+        public static final int
+                RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE =
+                3 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        /**
+         * Indicates that ProfileVerifier runs on an unsupported api version of Android.
+         * Note that ProfileVerifier supports only {@link Build.VERSION_CODES#P} and above.
+         * Note that when this result code is returned {@link #isCompiledWithProfile()} and
+         * {@link #hasProfileEnqueuedForCompilation()} return false.
+         */
+        public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION =
+                4 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        final int mResultCode;
+        private final boolean mHasReferenceProfile;
+        private final boolean mHasCurrentProfile;
+
+        CompilationStatus(
+                int resultCode,
+                boolean hasReferenceProfile,
+                boolean hasCurrentProfile
+        ) {
+            this.mResultCode = resultCode;
+            this.mHasCurrentProfile = hasCurrentProfile;
+            this.mHasReferenceProfile = hasReferenceProfile;
+        }
+
+        /**
+         * @return a result code that indicates whether there is a baseline profile installed and
+         * whether the app has been compiled with it. This depends on the installation method: if it
+         * was installed through app store or package manager the app gets compiled immediately
+         * with the profile and the return code is
+         * {@link CompilationStatus#RESULT_CODE_COMPILED_WITH_PROFILE},
+         * otherwise it'll be in `awaiting compilation` state and it'll be compiled at some point
+         * later in the future, so the return code will be
+         * {@link CompilationStatus#RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION}.
+         * In the case that no profile was installed, the result code will be
+         * {@link CompilationStatus#RESULT_CODE_NO_PROFILE}.
+         *
+         * Note that even if no profile was installed it's still possible for the app to have a
+         * profile and be compiled with it, as result of background dex optimization.
+         * The result code does a simple size check to ensure the compilation process completed
+         * without errors. If the size check fails this method will return
+         * {@link CompilationStatus#RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING}. The size
+         * check is
+         * not 100% accurate as the actual compiled methods are not checked.
+         *
+         * If something fails during the verification process, this method will return one of the
+         * result codes associated with an error.
+         *
+         * Note that only api 28 {@link Build.VERSION_CODES#P} and above is supported
+         * and that {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} is returned when calling
+         * this api on pre api 28.
+         */
+        @ResultCode
+        public int getProfileInstallResultCode() {
+            return mResultCode;
+        }
+
+        /**
+         * @return True whether this app has been compiled with a profile, false otherwise. An
+         * app can be compiled with a profile because of profile installation through app store,
+         * package manager or profileinstaller and subsequent background dex optimization. There
+         * should be a performance improvement when an app has been compiled with a profile. Note
+         * that if {@link #getProfileInstallResultCode()} returns
+         * {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} this method always returns
+         * always false.
+         */
+        public boolean isCompiledWithProfile() {
+            return mHasReferenceProfile;
+        }
+
+        /**
+         * @return True whether this app has a profile enqueued for compilation, false otherwise. An
+         * app can have a profile enqueued for compilation because of profile installation through
+         * profileinstaller or simply when the user starts interacting with the app. Note that if
+         * {@link #getProfileInstallResultCode()} returns
+         * {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} this method always returns false.
+         */
+        public boolean hasProfileEnqueuedForCompilation() {
+            return mHasCurrentProfile;
+        }
+    }
+
+    @RequiresApi(33)
+    private static class Api33Impl {
+        private Api33Impl() {
+        }
+
+        @DoNotInline
+        static PackageInfo getPackageInfo(
+                PackageManager packageManager,
+                Context context) throws PackageManager.NameNotFoundException {
+            return packageManager.getPackageInfo(
+                    context.getPackageName(),
+                    PackageManager.PackageInfoFlags.of(0)
+            );
+        }
+    }
+}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
index e98c680..783346a 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
@@ -18,9 +18,13 @@
 
 import android.os.Build;
 
+import androidx.annotation.RestrictTo;
+
 import java.util.Arrays;
 
-class ProfileVersion {
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ProfileVersion {
     private ProfileVersion() {}
     static final byte[] V015_S = new byte[]{'0', '1', '5', '\0'};
     static final byte[] V010_P = new byte[]{'0', '1', '0', '\0'};
@@ -29,7 +33,8 @@
     static final byte[] V001_N = new byte[]{'0', '0', '1', '\0'};
     static final byte[] METADATA_V001_N = new byte[]{'0', '0', '1', '\0'};
     static final byte[] METADATA_V002 = new byte[]{'0', '0', '2', '\0'};
-    static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
+    public static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
+    public static final int MAX_SUPPORTED_SDK = Build.VERSION_CODES.TIRAMISU;
 
     static String dexKeySeparator(byte[] version) {
         if (Arrays.equals(version, V001_N)) {
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index a083948..1649118 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -35,15 +35,19 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.StateListDrawable;
 import android.os.Build;
+import android.os.Bundle;
 import android.util.Log;
 import android.util.StateSet;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 
@@ -1268,7 +1272,6 @@
         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
     }
 
-
     @Test
     public void accessibilityPositions() throws Throwable {
         setupByConfig(new Config(VERTICAL, false, false), true);
@@ -1288,4 +1291,186 @@
                 event.getToIndex(),
                 mLayoutManager.findLastVisibleItemPosition());
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
+        final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+        assertFalse(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo);
+            }
+        });
+
+        assertFalse(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(1)), false);
+        final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+        assertFalse(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo);
+            }
+        });
+
+        assertTrue(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_withTooLowPosition()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+        assertFirstItemIsAtTop();
+
+        final boolean[] returnValue = {false};
+        Bundle arguments = new Bundle();
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mLayoutManager.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, arguments);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+
+        assertFalse(returnValue[0]);
+        assertFirstItemIsAtTop();
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_verticalWithNoRowArg()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+        assertFirstItemIsAtTop();
+
+        final boolean[] returnValue = {false};
+        Bundle arguments = new Bundle();
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 30);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mLayoutManager.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, arguments);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+
+        assertFalse(returnValue[0]);
+        assertFirstItemIsAtTop();
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_horizontalWithNoColumnArg()
+            throws Throwable {
+        setupByConfig(new Config(HORIZONTAL, false, false).adapter(new TestAdapter(30)), true);
+        assertFirstItemIsAtTop();
+
+        final boolean[] returnValue = {false};
+        Bundle arguments = new Bundle();
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mLayoutManager.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, arguments);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+
+        assertFalse(returnValue[0]);
+        assertFirstItemIsAtTop();
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_verticalWithRowArg_scrolls()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+        assertFirstItemIsAtTop();
+
+        final boolean[] returnValue = {false};
+        Bundle arguments = new Bundle();
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
+        // The column argument is ignored in VERTICAL orientation.
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 30);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mLayoutManager.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, arguments);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+
+        assertTrue(returnValue[0]);
+        assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (11)");
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_horizontalWithColumnArg_scrolls()
+            throws Throwable {
+        setupByConfig(new Config(HORIZONTAL, false, false).adapter(new TestAdapter(30)), true);
+        assertFirstItemIsAtTop();
+
+        final boolean[] returnValue = {false};
+        Bundle arguments = new Bundle();
+        // The row argument is ignored in HORIZONTAL orientation.
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 30);
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 10);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mLayoutManager.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, arguments);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+
+        assertTrue(returnValue[0]);
+        assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (11)");
+    }
+
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_withTooHighPosition_scrollsToEnd()
+            throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+        assertFirstItemIsAtTop();
+
+        final boolean[] returnValue = {false};
+        Bundle arguments = new Bundle();
+        arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 1000);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mLayoutManager.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, arguments);
+            }
+        });
+        mLayoutManager.waitForLayout(2);
+
+        assertTrue(returnValue[0]);
+        assertEquals(((TextView) mLayoutManager.getChildAt(
+                mLayoutManager.getChildCount() - 1)).getText(), "Item (30)");
+    }
+
+    private void assertFirstItemIsAtTop() {
+        assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (1)");
+    }
 }
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 9cf5e62..19a9b5f 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -21,6 +21,8 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.PointF;
+import android.os.Build;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.AttributeSet;
@@ -30,9 +32,12 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.core.os.TraceCompat;
 import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
 
 import java.util.List;
 
@@ -285,6 +290,56 @@
     }
 
     @Override
+    public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
+            @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
+        super.onInitializeAccessibilityNodeInfo(recycler, state, info);
+        // TODO(b/251823537)
+        if (mRecyclerView.mAdapter.getItemCount() > 0) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                info.addAction(AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
+            }
+        }
+    }
+
+    @Override
+    boolean performAccessibilityAction(int action, @Nullable Bundle args) {
+        if (super.performAccessibilityAction(action, args)) {
+            return true;
+        }
+
+        if (action == android.R.id.accessibilityActionScrollToPosition && args != null) {
+            int position = -1;
+
+            if (mOrientation == VERTICAL) {
+                final int rowArg = args.getInt(
+                        AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, -1);
+                if (rowArg < 0) {
+                    return false;
+                }
+                position = Math.min(rowArg, getRowCountForAccessibility(mRecyclerView.mRecycler,
+                        mRecyclerView.mState) - 1);
+            } else { // horizontal
+                final int columnArg = args.getInt(
+                        AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, -1);
+                if (columnArg < 0) {
+                    return false;
+                }
+                position = Math.min(columnArg,
+                        getColumnCountForAccessibility(mRecyclerView.mRecycler,
+                                mRecyclerView.mState) - 1);
+            }
+            if (position >= 0) {
+                // We want the target element to be the first on screen. That way, a
+                // screenreader like Talkback can directly focus on it as part of its default focus
+                // logic.
+                scrollToPositionWithOffset(position, 0);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
     @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
     public Parcelable onSaveInstanceState() {
         if (mPendingSavedState != null) {
diff --git a/settings.gradle b/settings.gradle
index 014208e..dbc189b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -571,7 +571,7 @@
 includeProject(":compose:ui:ui-viewbinding:ui-viewbinding-samples", "compose/ui/ui-viewbinding/samples", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui:integration-tests:ui-demos", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui:ui-samples", "compose/ui/ui/samples", [BuildType.COMPOSE])
-includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA])
+includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA, BuildType.COMPOSE])
 includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN, BuildType.CAMERA])
 includeProject(":constraintlayout:constraintlayout-compose", [BuildType.COMPOSE])
 includeProject(":constraintlayout:constraintlayout-compose:integration-tests:constraintlayout-compose-demos", [BuildType.COMPOSE])
@@ -782,9 +782,13 @@
 includeProject(":privacysandbox:tools:tools", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-apicompiler", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-apigenerator", [BuildType.MAIN])
+includeProject(":privacysandbox:tools:tools-apipackager", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-core", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-testing", [BuildType.MAIN])
 includeProject(":profileinstaller:profileinstaller", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":profileinstaller:integration-tests:profile-verification", [BuildType.MAIN])
+includeProject(":profileinstaller:integration-tests:profile-verification-sample", [BuildType.MAIN])
+includeProject(":profileinstaller:integration-tests:profile-verification-sample-no-initializer", [BuildType.MAIN])
 includeProject(":profileinstaller:integration-tests:init-macrobenchmark", [BuildType.MAIN])
 includeProject(":profileinstaller:integration-tests:init-macrobenchmark-target", [BuildType.MAIN])
 includeProject(":profileinstaller:profileinstaller-benchmark", [BuildType.MAIN])
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java
index 8addb3a..4468216 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java
@@ -16,39 +16,19 @@
 
 package androidx.test.uiautomator;
 
-import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.view.ViewConfiguration;
 
-/**
- * The {@link Gestures} class provides factory methods for constructing common
- * {@link PointerGesture}s.
- */
+/** Factory methods for constructing {@link PointerGesture}s. */
 class Gestures {
 
-    // Singleton instance
-    private static Gestures sInstance;
-
     // Constants used by pinch gestures
     private static final int INNER = 0;
     private static final int OUTER = 1;
     private static final int INNER_MARGIN = 5;
 
-    // Keep a handle to the ViewConfiguration
-    private ViewConfiguration mViewConfig;
-
-    // Private constructor.
-    private Gestures(ViewConfiguration config) {
-       mViewConfig = config;
-    }
-
-    /** Returns the {@link Gestures} instance for the given {@link Context}. */
-    public static Gestures getInstance(UiDevice device) {
-        if (sInstance == null) {
-            sInstance = new Gestures(ViewConfiguration.get(device.getUiContext()));
-        }
-        return sInstance;
+    private Gestures() {
     }
 
     /**
@@ -59,7 +39,7 @@
      * @param displayId The ID of display where {@code point} is on.
      * @return The {@link PointerGesture} representing this click.
      */
-    public PointerGesture click(Point point, int displayId) {
+    public static PointerGesture click(Point point, int displayId) {
         // A basic click is a touch down and touch up over the same point with no delay.
         return click(point, 0, displayId);
     }
@@ -73,7 +53,7 @@
      * @param displayId The ID of display where {@code point} is on.
      * @return The {@link PointerGesture} representing this click.
      */
-    public PointerGesture click(Point point, long duration, int displayId) {
+    public static PointerGesture click(Point point, long duration, int displayId) {
         // A click is a touch down and touch up over the same point with an optional delay inbetween
         return new PointerGesture(point, displayId).pause(duration);
     }
@@ -86,9 +66,9 @@
      * @param displayId The ID of display where {@code point} is on.
      * @return The {@link PointerGesture} representing this long click.
      */
-    public PointerGesture longClick(Point point, int displayId) {
+    public static PointerGesture longClick(Point point, int displayId) {
         // A long click is a click with a duration that exceeds a certain threshold.
-        return click(point, mViewConfig.getLongPressTimeout(), displayId);
+        return click(point, ViewConfiguration.getLongPressTimeout(), displayId);
     }
 
     /**
@@ -100,7 +80,7 @@
      * @param displayId The ID of display where the swipe is on.
      * @return The {@link PointerGesture} representing this swipe.
      */
-    public PointerGesture swipe(Point start, Point end, int speed, int displayId) {
+    public static PointerGesture swipe(Point start, Point end, int speed, int displayId) {
         // A swipe is a click that moves before releasing the pointer.
         return new PointerGesture(start, displayId).move(end, speed);
     }
@@ -110,12 +90,12 @@
      *
      * @param area The area to swipe over.
      * @param direction The direction in which to swipe.
-     * @param float percent The size of the swipe as a percentage of the total area.
+     * @param percent The size of the swipe as a percentage of the total area.
      * @param speed The speed at which to move in pixels per second.
      * @param displayId The ID of display where the swipe is on.
      * @return The {@link PointerGesture} representing this swipe.
      */
-    public PointerGesture swipeRect(
+    public static PointerGesture swipeRect(
             Rect area, Direction direction, float percent, int speed, int displayId) {
         Point start, end;
         // TODO: Reverse horizontal direction if locale is RTL
@@ -152,7 +132,7 @@
      * @param displayId The ID of display where a click and drag are on.
      * @return The {@link PointerGesture} representing this swipe.
      */
-    public PointerGesture drag(Point start, Point end, int speed, int displayId) {
+    public static PointerGesture drag(Point start, Point end, int speed, int displayId) {
         // A drag is a swipe that starts with a long click.
         return longClick(start, displayId).move(end, speed);
     }
@@ -160,13 +140,13 @@
     /**
      * Returns an array of {@link PointerGesture}s representing a pinch close.
      *
-     * @param bounds The area to pinch over.
+     * @param area The area to pinch over.
      * @param percent The size of the pinch as a percentage of the total area.
      * @param speed The speed at which to move in pixels per second.
      * @param displayId The ID of display where a pinch close is on.
      * @return An array containing the two PointerGestures representing this pinch.
      */
-    public PointerGesture[] pinchClose(Rect area, float percent, int speed, int displayId) {
+    public static PointerGesture[] pinchClose(Rect area, float percent, int speed, int displayId) {
         Point[] bottomLeft = new Point[2];
         Point[] topRight = new Point[2];
         calcPinchCoordinates(area, percent, bottomLeft, topRight);
@@ -182,13 +162,13 @@
     /**
      * Returns an array of {@link PointerGesture}s representing a pinch close.
      *
-     * @param bounds The area to pinch over.
+     * @param area The area to pinch over.
      * @param percent The size of the pinch as a percentage of the total area.
      * @param speed The speed at which to move in pixels per second.
      * @param displayId The ID of display where a pinch open is on.
      * @return An array containing the two PointerGestures representing this pinch.
      */
-    public PointerGesture[] pinchOpen(Rect area, float percent, int speed, int displayId) {
+    public static PointerGesture[] pinchOpen(Rect area, float percent, int speed, int displayId) {
         Point[] bottomLeft = new Point[2];
         Point[] topRight = new Point[2];
         calcPinchCoordinates(area, percent, bottomLeft, topRight);
@@ -202,7 +182,7 @@
     }
 
     /** Calculates the inner and outer coordinates used in a pinch gesture. */
-    private void calcPinchCoordinates(Rect area, float percent,
+    private static void calcPinchCoordinates(Rect area, float percent,
             Point[] bottomLeft, Point[] topRight) {
 
         int offsetX = (int)((area.width() - 2 * INNER_MARGIN) / 2 * percent);
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index cd13a92..20465f6 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -51,7 +51,6 @@
     private static final String TAG = UiObject2.class.getSimpleName();
 
     private UiDevice mDevice;
-    private Gestures mGestures;
     private GestureController mGestureController;
     private BySelector mSelector;  // Hold this mainly for debugging
     private AccessibilityNodeInfo mCachedNode;
@@ -83,7 +82,6 @@
         mDevice = device;
         mSelector = selector;
         mCachedNode = cachedNode;
-        mGestures = Gestures.getInstance(device);
         mGestureController = GestureController.getInstance(device);
         final DisplayManager dm =
                 (DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
@@ -429,7 +427,7 @@
     /** Clicks on this object. */
     public void click() {
         Log.v(TAG, String.format("click(center=%s)", getVisibleCenter()));
-        mGestureController.performGesture(mGestures.click(getVisibleCenter(), getDisplayId()));
+        mGestureController.performGesture(Gestures.click(getVisibleCenter(), getDisplayId()));
     }
 
     /**
@@ -441,7 +439,7 @@
     public void click(@NonNull Point point) {
         clipToGestureBounds(point);
         Log.v(TAG, String.format("click(point=%s)", point));
-        mGestureController.performGesture(mGestures.click(point, getDisplayId()));
+        mGestureController.performGesture(Gestures.click(point, getDisplayId()));
     }
 
     /** Performs a click on this object that lasts for {@code duration} milliseconds. */
@@ -449,7 +447,7 @@
         Log.v(TAG, String.format("click(center=%s,duration=%d)",
                 getVisibleCenter(), duration));
         mGestureController.performGesture(
-                mGestures.click(getVisibleCenter(), duration, getDisplayId()));
+                Gestures.click(getVisibleCenter(), duration, getDisplayId()));
     }
 
     /**
@@ -463,7 +461,7 @@
     public void click(@NonNull Point point, long duration) {
         clipToGestureBounds(point);
         Log.v(TAG, String.format("click(point=%s,duration=%d)", point, duration));
-        mGestureController.performGesture(mGestures.click(point, duration, getDisplayId()));
+        mGestureController.performGesture(Gestures.click(point, duration, getDisplayId()));
     }
 
     /** Clicks on this object, and waits for the given condition to become true. */
@@ -471,7 +469,7 @@
         Log.v(TAG, String.format("clickAndWait(center=%s,timeout=%d)",
                 getVisibleCenter(), timeout));
         return mGestureController.performGestureAndWait(condition, timeout,
-                mGestures.click(getVisibleCenter(), getDisplayId()));
+                Gestures.click(getVisibleCenter(), getDisplayId()));
     }
 
     /**
@@ -487,8 +485,8 @@
             long timeout) {
         clipToGestureBounds(point);
         Log.v(TAG, String.format("clickAndWait(point=%s,timeout=%d)", point, timeout));
-        return mGestureController.performGestureAndWait(condition, timeout,
-                mGestures.click(point, getDisplayId()));
+        return mGestureController.performGestureAndWait(
+                condition, timeout, Gestures.click(point, getDisplayId()));
     }
 
     /**
@@ -513,14 +511,14 @@
         Log.v(TAG, String.format("drag(start=%s,dest=%s,speed=%d)",
                 getVisibleCenter(), dest, speed));
         mGestureController.performGesture(
-                mGestures.drag(getVisibleCenter(), dest, speed, getDisplayId()));
+                Gestures.drag(getVisibleCenter(), dest, speed, getDisplayId()));
     }
 
     /** Performs a long click on this object. */
     public void longClick() {
         Log.v(TAG, String.format("longClick(center=%s)",
                 getVisibleCenter()));
-        mGestureController.performGesture(mGestures.longClick(getVisibleCenter(), getDisplayId()));
+        mGestureController.performGesture(Gestures.longClick(getVisibleCenter(), getDisplayId()));
     }
 
     /**
@@ -548,8 +546,7 @@
         Log.v(TAG, String.format("pinchClose(bounds=%s,percent=%f,speed=%d)",
                 getVisibleBoundsForGestures(), percent, speed));
         mGestureController.performGesture(
-                mGestures.pinchClose(
-                        getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
+                Gestures.pinchClose(getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
     }
 
     /**
@@ -577,8 +574,7 @@
         Log.v(TAG, String.format("pinchOpen(bounds=%s,percent=%f,speed=%d)",
                 getVisibleBoundsForGestures(), percent, speed));
         mGestureController.performGesture(
-                mGestures.pinchOpen(
-                        getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
+                Gestures.pinchOpen(getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
     }
 
     /**
@@ -609,7 +605,7 @@
         Log.v(TAG, String.format("swipe(bounds=%s,direction=%s,percent=%f,speed=%d)",
                 bounds, direction, percent, speed));
         mGestureController.performGesture(
-                mGestures.swipeRect(bounds, direction, percent, speed, getDisplayId()));
+                Gestures.swipeRect(bounds, direction, percent, speed, getDisplayId()));
     }
 
     /**
@@ -648,7 +644,7 @@
                 direction, bounds, percent, speed));
         for (; percent > 0.0f; percent -= 1.0f) {
             float segment = percent > 1.0f ? 1.0f : percent;
-            PointerGesture swipe = mGestures.swipeRect(
+            PointerGesture swipe = Gestures.swipeRect(
                     bounds, swipeDirection, segment, speed, getDisplayId()).pause(250);
 
             // Perform the gesture and return early if we reached the end
@@ -690,7 +686,7 @@
         Rect bounds = getVisibleBoundsForGestures();
         Log.v(TAG, String.format("fling(bounds=%s,direction=%s,speed=%d)",
                 bounds, direction, speed));
-        PointerGesture swipe = mGestures.swipeRect(
+        PointerGesture swipe = Gestures.swipeRect(
                 bounds, swipeDirection, 1.0f, speed, getDisplayId());
 
         // Perform the gesture and return true if we did not reach the end
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
index c3aa5d9..6207059 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
@@ -219,14 +219,14 @@
         )
     )
     BasicText(
-        text,
-        modifier,
-        mergedStyle,
-        onTextLayout,
-        overflow,
-        softWrap,
-        maxLines,
-        inlineContent
+        text = text,
+        modifier = modifier,
+        style = mergedStyle,
+        onTextLayout = onTextLayout,
+        overflow = overflow,
+        softWrap = softWrap,
+        maxLines = maxLines,
+        inlineContent = inlineContent
     )
 }
 
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index d385ad9..d6cae24 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -102,9 +102,9 @@
     ctor public LongTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? icon);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -137,8 +137,8 @@
     ctor public MonochromaticImageComplicationData.Builder(androidx.wear.watchface.complications.data.MonochromaticImage monochromaticImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -169,9 +169,9 @@
     ctor public NoPermissionComplicationData.Builder();
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -195,8 +195,8 @@
     ctor public PhotoImageComplicationData.Builder(android.graphics.drawable.Icon photoImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -238,9 +238,9 @@
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -267,9 +267,9 @@
     ctor public ShortTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -304,8 +304,8 @@
     ctor public SmallImageComplicationData.Builder(androidx.wear.watchface.complications.data.SmallImage smallImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 2b7bcf9..4f035e8 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -117,9 +117,9 @@
     ctor public DiscreteRangedValueComplicationData.Builder(int value, int min, int max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -158,9 +158,9 @@
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
     method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -186,8 +186,8 @@
     ctor public ListComplicationData.Builder(java.util.List<? extends androidx.wear.watchface.complications.data.ComplicationData> complicationList, androidx.wear.watchface.complications.data.ListComplicationData.StyleHint styleHint, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.ListComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -217,9 +217,9 @@
     ctor public LongTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? icon);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -252,8 +252,8 @@
     ctor public MonochromaticImageComplicationData.Builder(androidx.wear.watchface.complications.data.MonochromaticImage monochromaticImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -284,9 +284,9 @@
     ctor public NoPermissionComplicationData.Builder();
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -310,8 +310,8 @@
     ctor public PhotoImageComplicationData.Builder(android.graphics.drawable.Icon photoImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -345,8 +345,8 @@
     ctor public ProtoLayoutComplicationData.Builder(androidx.wear.tiles.LayoutElementBuilders.Layout ambientLayout, androidx.wear.tiles.LayoutElementBuilders.Layout interactiveLayout, androidx.wear.tiles.ResourceBuilders.Resources resources, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -379,9 +379,9 @@
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
     method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -408,9 +408,9 @@
     ctor public ShortTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -445,8 +445,8 @@
     ctor public SmallImageComplicationData.Builder(androidx.wear.watchface.complications.data.SmallImage smallImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -554,9 +554,9 @@
     ctor public WeightedElementsComplicationData.Builder(java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index efb6b9a..e27cc62 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -102,9 +102,9 @@
     ctor public LongTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? icon);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -137,8 +137,8 @@
     ctor public MonochromaticImageComplicationData.Builder(androidx.wear.watchface.complications.data.MonochromaticImage monochromaticImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -169,9 +169,9 @@
     ctor public NoPermissionComplicationData.Builder();
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
     method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -195,8 +195,8 @@
     ctor public PhotoImageComplicationData.Builder(android.graphics.drawable.Icon photoImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
@@ -239,9 +239,9 @@
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -268,9 +268,9 @@
     ctor public ShortTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -305,8 +305,8 @@
     ctor public SmallImageComplicationData.Builder(androidx.wear.watchface.complications.data.SmallImage smallImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData build();
     method public final T setDataSource(android.content.ComponentName? dataSource);
-    method public final T setDisplayPolicy(int displayPolicy);
-    method public final T setPersistencePolicy(int persistencePolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
     method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
   }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index d66bdf2..a5e86a9 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.ColorInt
 import androidx.annotation.IntDef
 import androidx.annotation.FloatRange
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.wear.tiles.LayoutElementBuilders
 import androidx.wear.tiles.ResourceBuilders
@@ -197,6 +198,7 @@
          * Sets the complication's [ComplicationPersistencePolicy].
          */
         @Suppress("UNCHECKED_CAST", "SetterReturnsThis")
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
         public fun setPersistencePolicy(@ComplicationPersistencePolicy persistencePolicy: Int): T {
             this.persistencePolicy = persistencePolicy
             return this as T
@@ -206,6 +208,7 @@
          * Sets the complication's [ComplicationDisplayPolicy].
          */
         @Suppress("UNCHECKED_CAST", "SetterReturnsThis")
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
         public fun setDisplayPolicy(@ComplicationDisplayPolicy displayPolicy: Int): T {
             this.displayPolicy = displayPolicy
             return this as T
@@ -3144,6 +3147,7 @@
 }
 
 @OptIn(ComplicationExperimental::class)
+@Suppress("NewApi")
 internal fun WireComplicationData.toPlaceholderComplicationData(): ComplicationData? {
     // Make sure we use the correct dataSource, persistencePolicy & displayPolicy.
     val dataSourceCopy = dataSource
@@ -3347,6 +3351,7 @@
  */
 @OptIn(ComplicationExperimental::class)
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Suppress("NewApi")
 public fun WireComplicationData.toApiComplicationData(): ComplicationData {
     // Make sure we use the correct dataSource, persistencePolicy & displayPolicy.
     val dataSourceCopy = dataSource
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 199c818..25c7f64 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -40,6 +40,7 @@
 import org.robolectric.shadows.ShadowLog
 
 @RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
 public class AsWireComplicationDataTest {
     val resources = ApplicationProvider.getApplicationContext<Context>().resources
     val dataSourceA = ComponentName("com.pkg_a", "com.a")
@@ -2298,6 +2299,7 @@
 }
 
 @RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
 public class FromWireComplicationDataTest {
     @Test
     public fun noDataComplicationData() {
@@ -3186,6 +3188,7 @@
 }
 
 @RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
 public class ValidTimeRangeTest {
     private val testStartInstant = Instant.ofEpochMilli(1000L)
     private val testEndDateInstant = Instant.ofEpochMilli(2000L)
diff --git a/wear/watchface/watchface/src/main/AndroidManifest.xml b/wear/watchface/watchface/src/main/AndroidManifest.xml
index 546fd68..546b36f 100644
--- a/wear/watchface/watchface/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface/src/main/AndroidManifest.xml
@@ -34,7 +34,7 @@
         android:enabled="@bool/watch_face_instance_service_enabled"
         android:exported="true"
         android:permission="com.google.android.wearable.permission.BIND_WATCH_FACE_CONTROL">
-      <meta-data android:name="androidx.wear.watchface.api_version" android:value="5" />
+      <meta-data android:name="androidx.wear.watchface.api_version" android:value="6" />
       <meta-data android:name="androidx.wear.watchface.xml_version"
           android:value="@integer/watch_face_xml_version" />
       <intent-filter>
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 417efe4..ea60837 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -2884,6 +2884,7 @@
     }
 
     @Test
+    @Suppress("NewApi")
     public fun complicationCachePolicy() {
         val complicationCache = HashMap<String, ByteArray>()
         val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
@@ -6146,6 +6147,7 @@
     }
 
     @Test
+    @Suppress("NewApi")
     public fun doNotDisplayComplicationWhenScreenLocked() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 14e674d..a926937 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -437,8 +437,8 @@
      *     <thead>
      *         <tr>
      *             <th>App</th>
-     *             <th>Web content which uses {@code prefers-color-scheme}</th>
-     *             <th>Web content which does not use {@code prefers-color-scheme}</th>
+     *             <th>Web content which uses prefers-color-scheme</th>
+     *             <th>Web content which does not use prefers-color-scheme</th>
      *         </tr>
      *     </thead>
      *     <tbody>